Merge m-c to b2ginbound a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Tue, 24 Mar 2015 18:36:38 -0700
changeset 264421 771b34ea01539f728959037997d1af6c3f4a7055
parent 264420 f037e9c31eed91736dd109947a8987ba24aec182 (current diff)
parent 264278 cc0950b7a3696f925e2f0c917649aa537f59aa27 (diff)
child 264422 f4e6c6c0d76881b67092f9435d1926bd8b1df3eb
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone39.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 b2ginbound a=merge CLOSED TREE
--- a/addon-sdk/source/app-extension/bootstrap.js
+++ b/addon-sdk/source/app-extension/bootstrap.js
@@ -349,15 +349,14 @@ function nukeModules() {
   }
   loader = null;
 
   // both `toolkit/loader` and `system/xul-app` are loaded as JSM's via
   // `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when
   // the addon is unload.
 
   unloadSandbox(cuddlefishSandbox.loaderSandbox);
-  unloadSandbox(cuddlefishSandbox.xulappSandbox);
 
   // Bug 764840: We need to unload cuddlefish otherwise it will stay alive
   // and keep a reference to this compartment.
   unloadSandbox(cuddlefishSandbox);
   cuddlefishSandbox = null;
 }
--- a/addon-sdk/source/bin/jpm-test.js
+++ b/addon-sdk/source/bin/jpm-test.js
@@ -1,27 +1,29 @@
 /* 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 readParam = require("./node-scripts/utils").readParam;
-var path = require("path");
+var Promise = require("promise");
 var Mocha = require("mocha");
 var mocha = new Mocha({
   ui: "bdd",
   reporter: "spec",
   timeout: 900000
 });
 
-var type = readParam("type");
+exports.run = function(type) {
+  return new Promise(function(resolve) {
+    type = type || "";
+    [
+      (/^(modules)?$/.test(type)) && require.resolve("../bin/node-scripts/test.modules"),
+      (/^(addons)?$/.test(type)) && require.resolve("../bin/node-scripts/test.addons"),
+      (/^(examples)?$/.test(type)) && require.resolve("../bin/node-scripts/test.examples"),
+    ].sort().forEach(function(filepath) {
+      filepath && mocha.addFile(filepath);
+    })
 
-[
-  (!type || type == "modules") && require.resolve("../bin/node-scripts/test.modules"),
-  (!type || type == "addons") && require.resolve("../bin/node-scripts/test.addons"),
-  (!type || type == "examples") && require.resolve("../bin/node-scripts/test.examples"),
-].sort().forEach(function(filepath) {
-  filepath && mocha.addFile(filepath);
-})
-
-mocha.run(function (failures) {
-  process.exit(failures);
-});
+    mocha.run(function(failures) {
+      resolve(failures);
+    });
+  });
+}
--- a/addon-sdk/source/examples/debug-client/data/client.js
+++ b/addon-sdk/source/examples/debug-client/data/client.js
@@ -197,17 +197,17 @@ var Connection = Class({
   addPool: function(pool) {
     this.pools.add(pool);
   },
   removePool: function(pool) {
     this.pools.delete(pool);
   },
   poolFor: function(id) {
     for (let pool of this.pools.values()) {
-      if (pool.has(id))
+      if pool.has(id)
         return pool;
     }
   },
   get: function(id) {
     var pool = this.poolFor(id);
     return pool && pool.get(id);
   },
   disconnect: function() {
@@ -792,17 +792,17 @@ var Tab = Client.from({
     "canvasActor": "canvas",
     "webglActor": "webgl",
     "webaudioActor": "webaudio",
     "styleSheetsActor": "stylesheets",
     "styleEditorActor": "styleeditor",
     "storageActor": "storage",
     "gcliActor": "gcli",
     "memoryActor": "memory",
-    "eventLoopLag": "eventLoopLag",
+    "eventLoopLag": "eventLoopLag"
 
     "trace": "trace", // missing
   }
 });
 
 var tablist = Client.from({
   "category": "dict",
   "typeName": "tablist",
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/gulpfile.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";
+
+var gulp = require('gulp');
+
+gulp.task('test', function(done) {
+  require("./bin/jpm-test").run().then(done);
+});
+
+gulp.task('test:addons', function(done) {
+  require("./bin/jpm-test").run("addons").then(done);
+});
+
+gulp.task('test:examples', function(done) {
+  require("./bin/jpm-test").run("examples").then(done);
+});
+
+gulp.task('test:modules', function(done) {
+  require("./bin/jpm-test").run("modules").then(done);
+});
+
--- a/addon-sdk/source/lib/sdk/content/l10n-html.js
+++ b/addon-sdk/source/lib/sdk/content/l10n-html.js
@@ -11,32 +11,59 @@ const { Ci } = require("chrome");
 const core = require("../l10n/core");
 const { loadSheet, removeSheet } = require("../stylesheet/utils");
 const { process, frames } = require("../remote/child");
 
 const assetsURI = require('../self').data.url();
 
 const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
 
+function translateElementAttributes(element) {
+  // Translateable attributes
+  const attrList = ['title', 'accesskey', 'alt', 'label', 'placeholder'];
+  const ariaAttrMap = {
+          'ariaLabel': 'aria-label',
+          'ariaValueText': 'aria-valuetext',
+          'ariaMozHint': 'aria-moz-hint'
+        };
+  const attrSeparator = '.';
+  
+  // Try to translate each of the attributes
+  for (let attribute of attrList) {
+    const data = core.get(element.dataset.l10nId + attrSeparator + attribute);
+    if (data)
+      element.setAttribute(attribute, data);
+  }
+  
+  // Look for the aria attribute translations that match fxOS's aliases
+  for (let attrAlias in ariaAttrMap) {
+    const data = core.get(element.dataset.l10nId + attrSeparator + attrAlias);
+    if (data)
+      element.setAttribute(ariaAttrMap[attrAlias], data);
+  }
+}
+
 // Taken from Gaia:
 // https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
 function translateElement(element) {
   element = element || document;
 
   // check all translatable children (= w/ a `data-l10n-id' attribute)
   var children = element.querySelectorAll('*[data-l10n-id]');
   var elementCount = children.length;
   for (var i = 0; i < elementCount; i++) {
     var child = children[i];
 
     // translate the child
     var key = child.dataset.l10nId;
     var data = core.get(key);
     if (data)
       child.textContent = data;
+
+    translateElementAttributes(child);
   }
 }
 exports.translateElement = translateElement;
 
 function onDocumentReady2Translate(event) {
   let document = event.target;
   document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
                                false);
--- a/addon-sdk/source/lib/sdk/deprecated/unit-test.js
+++ b/addon-sdk/source/lib/sdk/deprecated/unit-test.js
@@ -315,16 +315,18 @@ TestRunner.prototype = {
           win.removeEventListener("DOMContentLoaded", onLoad, false);
           resolve();
         }, false);
       }
       return promise;
     });
 
     PromiseDebugging.flushUncaughtErrors();
+    PromiseDebugging.removeUncaughtErrorObserver(this._uncaughtErrorObserver);
+
 
     return all(winPromises).then(() => {
       let browserWins = wins.filter(isBrowser);
       let tabs = browserWins.reduce((tabs, window) => tabs.concat(getTabs(window)), []);
       let newTabID = getTabId(getSelectedTab(wins[0]));
       let oldTabID = runnerTabs.get(this);
       let hasMoreTabsOpen = browserWins.length && tabs.length != 1;
       let failure = false;
@@ -532,17 +534,18 @@ TestRunner.prototype = {
 
   start: function start(options) {
     this.test = options.test;
     this.test.passed = 0;
     this.test.failed = 0;
     this.test.errors = {};
     this.test.last = 'START';
     PromiseDebugging.clearUncaughtErrorObservers();
-    PromiseDebugging.addUncaughtErrorObserver(this._uncaughtErrorObserver.bind(this));
+    this._uncaughtErrorObserver = this._uncaughtErrorObserver.bind(this);
+    PromiseDebugging.addUncaughtErrorObserver(this._uncaughtErrorObserver);
 
     this.isDone = false;
     this.onDone = function(self) {
       if (cfxArgs.parseable)
         self.console.print("TEST-END | " + self.test.name + "\n");
       options.onDone(self);
     }
     this.waitTimeout = null;
--- a/addon-sdk/source/lib/sdk/event/dom.js
+++ b/addon-sdk/source/lib/sdk/event/dom.js
@@ -16,16 +16,20 @@ let listeners = new Map();
 
 let getWindowFrom = x =>
                     x instanceof Ci.nsIDOMWindow ? x :
                     x instanceof Ci.nsIDOMDocument ? x.defaultView :
                     x instanceof Ci.nsIDOMNode ? x.ownerDocument.defaultView :
                     null;
 
 function removeFromListeners() {
+  this.removeEventListener("DOMWindowClose", removeFromListeners);
+  for (let cleaner of listeners.get(this))
+    cleaner();
+
   listeners.delete(this);
 }
 
 // Simple utility function takes event target, event type and optional
 // `options.capture` and returns node style event stream that emits "data"
 // events every time event of that type occurs on the given `target`.
 function open(target, type, options) {
   let output = {};
@@ -40,31 +44,30 @@ function open(target, type, options) {
 
   // If we're not able to get a `window` from `target`, there is something
   // wrong. We cannot add listeners that can leak later, or results in
   // "dead object" exception.
   // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1001833
   if (!window)
     throw new Error("Unable to obtain the owner window from the target given.");
 
-  let cleaners = listeners.get(window) || [];
-  cleaners.push(() => target.removeEventListener(type, listener, capture));
-
-  listeners.set(window, cleaners);
+  let cleaners = listeners.get(window);
+  if (!cleaners) {
+    cleaners = [];
+    listeners.set(window, cleaners);
 
-  // We need to remove from our map the `window` once is closed, to prevent
-  // memory leak
-  window.addEventListener("DOMWindowClose", removeFromListeners);
+    // We need to remove from our map the `window` once is closed, to prevent
+    // memory leak
+    window.addEventListener("DOMWindowClose", removeFromListeners);
+  }
 
+  cleaners.push(() => target.removeEventListener(type, listener, capture));
   target.addEventListener(type, listener, capture);
 
   return output;
 }
 
 unload(() => {
-  for (let [window, cleaners] of listeners) {
-    cleaners.forEach(callback => callback())
-  }
-
-  listeners.clear();
+  for (let window of listeners.keys())
+    removeFromListeners.call(window);
 });
 
 exports.open = open;
--- a/addon-sdk/source/lib/sdk/io/fs.js
+++ b/addon-sdk/source/lib/sdk/io/fs.js
@@ -687,18 +687,16 @@ exports.open = open;
  */
 function writeSync(fd, buffer, offset, length, position) {
   if (length + offset > buffer.length) {
     throw Error("Length is extends beyond buffer");
   }
   else if (length + offset !== buffer.length) {
     buffer = buffer.slice(offset, offset + length);
   }
-  let writeStream = new WriteStream(fd, { position: position,
-                                          length: length });
 
   let output = BinaryOutputStream(nsIFileOutputStream(fd));
   nsIBinaryOutputStream(fd, output);
   // We write content as a byte array as this will avoid any transcoding
   // if content was a buffer.
   output.writeByteArray(buffer.valueOf(), buffer.length);
   output.flush();
 };
--- a/addon-sdk/source/lib/sdk/places/events.js
+++ b/addon-sdk/source/lib/sdk/places/events.js
@@ -18,16 +18,17 @@ const { Class } = require('../core/herit
 const { merge } = require('../util/object');
 const bookmarkService = Cc['@mozilla.org/browser/nav-bookmarks-service;1']
                         .getService(Ci.nsINavBookmarksService);
 const historyService = Cc['@mozilla.org/browser/nav-history-service;1']
                        .getService(Ci.nsINavHistoryService);
 const { mapBookmarkItemType } = require('./utils');
 const { EventTarget } = require('../event/target');
 const { emit } = require('../event/core');
+const { when } = require('../system/unload');
 
 const emitter = EventTarget();
 
 let HISTORY_ARGS = {
   onBeginUpdateBatch: [],
   onEndUpdateBatch: [],
   onClearHistory: [],
   onDeleteURI: ['url'],
@@ -114,9 +115,14 @@ function formatValue (type, data) {
 }
 
 let historyObserver = createObserverInstance(HISTORY_EVENTS, HISTORY_ARGS);
 historyService.addObserver(historyObserver, false);
 
 let bookmarkObserver = createObserverInstance(BOOKMARK_EVENTS, BOOKMARK_ARGS);
 bookmarkService.addObserver(bookmarkObserver, false);
 
+when(() => {
+  historyService.removeObserver(historyObserver);
+  bookmarkService.removeObserver(bookmarkObserver);
+});
+
 exports.events = emitter;
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -164,16 +164,25 @@ function serializeStack(frames) {
            frame.lineNumber + ":" +
            frame.columnNumber + "\n" +
            stack;
   }, "");
 }
 Loader.serializeStack = serializeStack;
 
 function readURI(uri) {
+  let nsURI = NetUtil.newURI(uri);
+  if (nsURI.scheme == "resource") {
+    // Resolve to a real URI, this will catch any obvious bad paths without
+    // logging assertions in debug builds, see bug 1135219
+    let proto = Cc["@mozilla.org/network/protocol;1?name=resource"].
+                getService(Ci.nsIResProtocolHandler);
+    uri = proto.resolveURI(nsURI);
+  }
+
   let stream = NetUtil.newChannel2(uri,
                                    'UTF-8',
                                    null,
                                    null,      // aLoadingNode
                                    systemPrincipal,
                                    null,      // aTriggeringPrincipal
                                    Ci.nsILoadInfo.SEC_NORMAL,
                                    Ci.nsIContentPolicy.TYPE_OTHER).open();
@@ -415,30 +424,39 @@ Loader.resolve = resolve;
 // Takes an id and path and attempts to load a file using node's resolving
 // algorithm.
 // `id` should already be resolved relatively at this point.
 // http://nodejs.org/api/modules.html#modules_all_together
 const nodeResolve = iced(function nodeResolve(id, requirer, { rootURI }) {
   // Resolve again
   id = Loader.resolve(id, requirer);
 
+  // If this is already an absolute URI then there is no resolution to do
+  if (isAbsoluteURI(id))
+    return void 0;
+
   // we assume that extensions are correct, i.e., a directory doesnt't have '.js'
   // and a js file isn't named 'file.json.js'
   let fullId = join(rootURI, id);
   let resolvedPath;
 
   if ((resolvedPath = loadAsFile(fullId)))
     return stripBase(rootURI, resolvedPath);
 
   if ((resolvedPath = loadAsDirectory(fullId)))
     return stripBase(rootURI, resolvedPath);
 
+  // If the requirer is an absolute URI then the node module resolution below
+  // won't work correctly as we prefix everything with rootURI
+  if (isAbsoluteURI(requirer))
+    return void 0;
+
   // If manifest has dependencies, attempt to look up node modules
   // in the `dependencies` list
-  let dirs = getNodeModulePaths(dirname(join(rootURI, requirer))).map(dir => join(dir, id));
+  let dirs = getNodeModulePaths(dirname(requirer)).map(dir => join(rootURI, dir, id));
   for (let i = 0; i < dirs.length; i++) {
     if ((resolvedPath = loadAsFile(dirs[i])))
       return stripBase(rootURI, resolvedPath);
 
     if ((resolvedPath = loadAsDirectory(dirs[i])))
       return stripBase(rootURI, resolvedPath);
   }
 
@@ -504,16 +522,17 @@ function getNodeModulePaths (start) {
 
   let parts = start.split('/');
   let dirs = [];
   for (let i = parts.length - 1; i >= 0; i--) {
     if (parts[i] === moduleDir) continue;
     let dir = join(parts.slice(0, i + 1).join('/'), moduleDir);
     dirs.push(dir);
   }
+  dirs.push(moduleDir);
   return dirs;
 }
 
 
 function addTrailingSlash (path) {
   return !path ? null : !path.endsWith('/') ? path + '/' : path;
 }
 
--- a/addon-sdk/source/package.json
+++ b/addon-sdk/source/package.json
@@ -3,33 +3,31 @@
   "description": "Add-on development made easy.",
   "keywords": [
     "javascript", "engine", "addon", "extension",
     "xulrunner", "firefox", "browser"
   ],
   "license": "MPL 2.0",
   "unpack": true,
   "scripts": {
-    "test": "node ./bin/jpm-test.js",
-    "modules": "node ./bin/jpm-test.js --type modules",
-    "addons": "node ./bin/jpm-test.js --type addons",
-    "examples": "node ./bin/jpm-test.js --type examples"
+    "test": "gulp test"
   },
   "homepage": "https://github.com/mozilla/addon-sdk",
   "repository": {
     "type": "git",
     "url": "git://github.com/mozilla/addon-sdk.git"
   },
   "version": "0.1.18",
   "main": "./lib/index.js",
   "loader": "lib/sdk/loader/cuddlefish.js",
   "devDependencies": {
     "async": "0.9.0",
     "chai": "2.1.1",
     "glob": "4.4.2",
+    "gulp": "3.8.11",
     "jpm": "0.0.29",
     "lodash": "3.3.1",
     "mocha": "2.1.0",
     "promise": "6.1.0",
     "rimraf": "2.3.1",
     "unzip": "0.1.11",
     "xmldom": "0.1.19"
   }
--- a/addon-sdk/source/python-lib/cuddlefish/prefs.py
+++ b/addon-sdk/source/python-lib/cuddlefish/prefs.py
@@ -51,16 +51,17 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
     'media.gmp-gmpopenh264.autoupdate' : False,
     'media.gmp-manager.cert.checkAttributes' : False,
     'media.gmp-manager.cert.requireBuiltIn' : False,
     'media.gmp-manager.url' : 'http://localhost/media-dummy/gmpmanager',
     'media.gmp-manager.url.override': 'http://localhost/dummy-gmp-manager.xml',
     'browser.aboutHomeSnippets.updateUrl': 'https://localhost/snippet-dummy',
     'browser.newtab.url' : 'about:blank',
     'browser.search.update': False,
+    'browser.search.suggest.enabled' : False,
     'browser.safebrowsing.enabled' : False,
     'browser.safebrowsing.updateURL': 'http://localhost/safebrowsing-dummy/update',
     'browser.safebrowsing.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
     'browser.safebrowsing.reportURL': 'http://localhost/safebrowsing-dummy/report',
     'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport',
     'browser.selfsupport.url': 'http://localhost/repair-dummy',
     'browser.trackingprotection.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
     'browser.trackingprotection.updateURL': 'http://localhost/safebrowsing-dummy/update',
--- a/addon-sdk/source/test/addons/e10s-l10n/data/test-localization.html
+++ b/addon-sdk/source/test/addons/e10s-l10n/data/test-localization.html
@@ -15,10 +15,15 @@
         Elements with data-l10n-id attribute whose parent element is translated
         will be replaced by the content of the translation.
       </li>
     </ul>
     <div data-l10n-id="text-content">No</div>
     <div data-l10n-id="Translated">
       A data-l10n-id value can be used in multiple elements
     </div>
+    <a data-l10n-id="link-attributes" title="Certain whitelisted attributes get translated too" alt="No" accesskey="A"></a>
+    <input data-l10n-id="input" type="text" placeholder="Form placeholders are translateable">
+    <menu>
+      <menuitem data-l10n-id="contextitem" label="Labels of select options and context menus are translateable">
+    </menu>
   </body>
 </html
--- a/addon-sdk/source/test/addons/e10s-l10n/locale/en.properties
+++ b/addon-sdk/source/test/addons/e10s-l10n/locale/en.properties
@@ -21,8 +21,18 @@ explicitPlural[other]=other
 unicodeEscape = \u0020\u0040\u0020
 # this string equals to " @ "
 
 # bug 1033309 plurals with multiple placeholders
 first_identifier[one]=first entry is %s and the second one is %s.
 first_identifier=the entries are %s and %s.
 second_identifier[other]=first entry is %s and the second one is %s.
 third_identifier=first entry is %s and the second one is %s.
+
+# bug 824489 allow translation of element attributes
+link-attributes.title=Yes
+link-attributes.alt=Yes
+link-attributes.accesskey=B
+input.placeholder=Yes
+contextitem.label=Yes
+link-attributes.ariaLabel=Yes
+link-attributes.ariaValueText=Value
+link-attributes.ariaMozHint=Hint
--- a/addon-sdk/source/test/addons/e10s-l10n/main.js
+++ b/addon-sdk/source/test/addons/e10s-l10n/main.js
@@ -100,32 +100,53 @@ exports.testHtmlLocalizationPageWorker =
   let uri = require("sdk/self").data.url("test-localization.html");
   let worker = loader.require("sdk/page-worker").Page({
     contentURL: uri,
     contentScript: "new " + function ContentScriptScope() {
       let nodes = document.body.querySelectorAll("*[data-l10n-id]");
       self.postMessage([nodes[0].innerHTML,
                         nodes[1].innerHTML,
                         nodes[2].innerHTML,
-                        nodes[3].innerHTML]);
+                        nodes[3].innerHTML,
+                        nodes[4].title,
+                        nodes[4].getAttribute("alt"),
+                        nodes[4].getAttribute("accesskey"),
+                        nodes[4].getAttribute("aria-label"),
+                        nodes[4].getAttribute("aria-valuetext"),
+                        nodes[4].getAttribute("aria-moz-hint"),
+                        nodes[5].placeholder,
+                        nodes[6].label]);
     },
     onMessage: function (data) {
       assert.equal(
         data[0],
         "Kept as-is",
         "Nodes with unknown id in .properties are kept 'as-is'"
       );
       assert.equal(data[1], "Yes", "HTML is translated");
       assert.equal(
         data[2],
         "no &lt;b&gt;HTML&lt;/b&gt; injection",
         "Content from .properties is text content; HTML can't be injected."
       );
       assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
 
+      // Attribute translation tests
+      assert.equal(data[4], "Yes", "Title attributes gets translated.");
+      assert.equal(data[5], "Yes", "Alt attributes gets translated.");
+      assert.equal(data[6], "B", "Accesskey gets translated.");
+      
+      assert.equal(data[7], "Yes", "Aria-Label gets translated.");
+      assert.equal(data[8], "Value", "Aria-valuetext gets translated.");
+      assert.equal(data[9], "Hint", "Aria-moz-hint gets translated.");
+      
+      assert.equal(data[10], "Yes", "Form placeholders are translateable.");
+      
+      assert.equal(data[11], "Yes", "Labels of select options and context menus are translateable.");
+
       done();
     }
   });
 });
 
 exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done) {
   // Ensure initing html component that watch document creations
   // Note that this module is automatically initialized in
@@ -139,32 +160,53 @@ exports.testHtmlLocalization = createTes
     onReady: function(tab) {
       tab.attach({
         contentURL: uri,
         contentScript: "new " + function ContentScriptScope() {
           let nodes = document.body.querySelectorAll("*[data-l10n-id]");
           self.postMessage([nodes[0].innerHTML,
                             nodes[1].innerHTML,
                             nodes[2].innerHTML,
-                            nodes[3].innerHTML]);
+                            nodes[3].innerHTML,
+                            nodes[4].title,
+                            nodes[4].getAttribute("alt"),
+                            nodes[4].getAttribute("accesskey"),
+                            nodes[4].getAttribute("aria-label"),
+                            nodes[4].getAttribute("aria-valuetext"),
+                            nodes[4].getAttribute("aria-moz-hint"),
+                            nodes[5].placeholder,
+                            nodes[6].label]);
         },
         onMessage: function (data) {
           assert.equal(
             data[0],
             "Kept as-is",
             "Nodes with unknown id in .properties are kept 'as-is'"
           );
           assert.equal(data[1], "Yes", "HTML is translated");
           assert.equal(
             data[2],
             "no &lt;b&gt;HTML&lt;/b&gt; injection",
             "Content from .properties is text content; HTML can't be injected."
           );
           assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
 
+          // Attribute translation tests
+          assert.equal(data[4], "Yes", "Title attributes gets translated.");
+          assert.equal(data[5], "Yes", "Alt attributes gets translated.");
+          assert.equal(data[6], "B", "Accesskey gets translated.");
+      
+          assert.equal(data[7], "Yes", "Aria-Label gets translated.");
+          assert.equal(data[8], "Value", "Aria-valuetext gets translated.");
+          assert.equal(data[9], "Hint", "Aria-moz-hint gets translated.");
+      
+          assert.equal(data[10], "Yes", "Form placeholders are translateable.");
+      
+          assert.equal(data[11], "Yes", "Labels of select options and context menus are translateable.");
+
           tab.close(done);
         }
       });
     }
   });
 });
 
 exports.testEnUsLocaleName = createTest("en-US", function(assert, loader, done) {
--- a/addon-sdk/source/test/addons/e10s-remote/main.js
+++ b/addon-sdk/source/test/addons/e10s-remote/main.js
@@ -1,31 +1,44 @@
 /* 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 LOCAL_URI = "about:robots";
-const REMOTE_URI = "about:home";
+const REMOTE_URI = "data:text/html;charset=utf-8,remote";
 
 const { Loader } = require('sdk/test/loader');
 const { getTabs, openTab, closeTab, setTabURL, getBrowserForTab, getURI } = require('sdk/tabs/utils');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { cleanUI } = require("sdk/test/utils");
 const { setTimeout } = require("sdk/timers");
 const { promiseEvent, promiseDOMEvent, promiseEventOnItemAndContainer,
         waitForProcesses, getChildFrameCount, isE10S } = require("./utils");
 const { after } = require('sdk/test/utils');
 const { processID } = require('sdk/system/runtime');
 
 const { set } = require('sdk/preferences/service');
 // The hidden preload browser messes up our frame counts
 set('browser.newtab.preload', false);
 
+function promiseTabFrameAttach(frames) {
+  return new Promise(resolve => {
+    let listener = function(frame, ...args) {
+      if (!frame.isTab)
+        return;
+      frames.off("attach", listener);
+      resolve([frame, ...args]);
+    }
+
+    frames.on("attach", listener);
+  });
+}
+
 // Check that we see a process stop and start
 exports["test process restart"] = function*(assert) {
   if (!isE10S) {
     assert.pass("Skipping test in non-e10s mode");
     return;
   }
 
   let window = getMostRecentBrowserWindow();
@@ -39,30 +52,30 @@ exports["test process restart"] = functi
 
   let remoteProcess = Array.filter(processes, p => p.isRemote)[0];
   let localProcess = Array.filter(processes, p => !p.isRemote)[0];
   let remoteFrame = Array.filter(frames, f => f.process == remoteProcess)[0];
 
   // Switch the remote tab to a local URI which should kill the remote process
 
   let frameDetach = promiseEventOnItemAndContainer(assert, remoteFrame, frames, 'detach');
-  let frameAttach = promiseEvent(frames, 'attach');
+  let frameAttach = promiseTabFrameAttach(frames);
   let processDetach = promiseEventOnItemAndContainer(assert, remoteProcess, processes, 'detach');
   setTabURL(tab, LOCAL_URI);
   // The load should kill the remote frame
   yield frameDetach;
   // And create a new frame in the local process
   let [newFrame] = yield frameAttach;
   assert.equal(newFrame.process, localProcess, "New frame should be in the local process");
   // And kill the process
   yield processDetach;
 
   frameDetach = promiseEventOnItemAndContainer(assert, newFrame, frames, 'detach');
   let processAttach = promiseEvent(processes, 'attach');
-  frameAttach = promiseEvent(frames, 'attach');
+  frameAttach = promiseTabFrameAttach(frames);
   setTabURL(tab, REMOTE_URI);
   // The load should kill the remote frame
   yield frameDetach;
   // And create a new remote process
   [remoteProcess] = yield processAttach;
   assert.ok(remoteProcess.isRemote, "Process should be remote");
   // And create a new frame in the remote process
   [newFrame] = yield frameAttach;
@@ -144,26 +157,26 @@ exports["test frame list"] = function*(a
   assert.equal(tabs.length, 1, "Should have just the one tab to start with");
 
   let loader = new Loader(module);
   let { processes, frames } = yield waitForProcesses(loader);
 
   assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
   assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab1 = openTab(window, LOCAL_URI);
   let [frame1] = yield promise;
   assert.ok(!!frame1, "Should have seen the new frame");
   assert.ok(!frame1.process.isRemote, "Frame should not be remote");
 
   assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
   assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
 
-  promise = promiseEvent(frames, 'attach');
+  promise = promiseTabFrameAttach(frames);
   let tab2 = openTab(window, REMOTE_URI);
   let [frame2] = yield promise;
   assert.ok(!!frame2, "Should have seen the new frame");
   if (isE10S)
     assert.ok(frame2.process.isRemote, "Frame should be remote");
   else
     assert.ok(!frame2.process.isRemote, "Frame should not be remote");
 
@@ -251,17 +264,17 @@ exports["test new loader"] = function*(a
 };
 
 // Test that unloading the loader unloads the child instances
 exports["test unload"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab = openTab(window, "data:,<html/>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   assert.ok(!!frame, "Should have seen the new frame");
 
   promise = promiseDOMEvent(browser, 'hashchange');
   frame.port.emit('sdk/test/testunload');
@@ -275,17 +288,17 @@ exports["test unload"] = function*(asser
 }
 
 // Test that unloading the loader causes the child to see frame detach events
 exports["test frame detach on unload"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab = openTab(window, "data:,<html/>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   assert.ok(!!frame, "Should have seen the new frame");
 
   promise = promiseDOMEvent(browser, 'hashchange');
   frame.port.emit('sdk/test/testdetachonunload');
@@ -299,17 +312,17 @@ exports["test frame detach on unload"] =
 }
 
 // Test that DOM event listener on the frame object works
 exports["test frame event listeners"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab = openTab(window, "data:text/html,<html></html>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   assert.ok(!!frame, "Should have seen the new frame");
 
   frame.port.emit('sdk/test/registerframeevent');
   promise = Promise.all([
@@ -334,17 +347,17 @@ exports["test frame event listeners"] = 
 }
 
 // Test that DOM event listener on the frames object works
 exports["test frames event listeners"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab = openTab(window, "data:text/html,<html></html>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   assert.ok(!!frame, "Should have seen the new frame");
 
   frame.port.emit('sdk/test/registerframesevent');
   promise = Promise.all([
@@ -372,18 +385,18 @@ exports["test frames event listeners"] =
 exports["test unload removes frame event listeners"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
   let loader2 = new Loader(module);
   let { frames: frames2 } = yield waitForProcesses(loader2);
 
-  let promise = promiseEvent(frames, 'attach');
-  let promise2 = promiseEvent(frames2, 'attach');
+  let promise = promiseTabFrameAttach(frames);
+  let promise2 = promiseTabFrameAttach(frames2);
   let tab = openTab(window, "data:text/html,<html></html>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   let [frame2] = yield promise2;
   assert.ok(!!frame && !!frame2, "Should have seen the new frame");
 
   frame.port.emit('sdk/test/registerframeevent');
@@ -413,18 +426,18 @@ exports["test unload removes frame event
 exports["test unload removes frames event listeners"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
   let loader2 = new Loader(module);
   let { frames: frames2 } = yield waitForProcesses(loader2);
 
-  let promise = promiseEvent(frames, 'attach');
-  let promise2 = promiseEvent(frames2, 'attach');
+  let promise = promiseTabFrameAttach(frames);
+  let promise2 = promiseTabFrameAttach(frames2);
   let tab = openTab(window, "data:text/html,<html></html>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   let [frame2] = yield promise2;
   assert.ok(!!frame && !!frame2, "Should have seen the new frame");
 
   frame.port.emit('sdk/test/registerframesevent');
--- a/addon-sdk/source/test/addons/l10n/data/test-localization.html
+++ b/addon-sdk/source/test/addons/l10n/data/test-localization.html
@@ -15,10 +15,15 @@
         Elements with data-l10n-id attribute whose parent element is translated
         will be replaced by the content of the translation.
       </li>
     </ul>
     <div data-l10n-id="text-content">No</div>
     <div data-l10n-id="Translated">
       A data-l10n-id value can be used in multiple elements
     </div>
+    <a data-l10n-id="link-attributes" title="Certain whitelisted attributes get translated too" alt="No" accesskey="A"></a>
+    <input data-l10n-id="input" type="text" placeholder="Form placeholders are translateable">
+    <menu>
+      <menuitem data-l10n-id="contextitem" label="Labels of select options and context menus are translateable">
+    </menu>
   </body>
 </html
--- a/addon-sdk/source/test/addons/l10n/locale/en.properties
+++ b/addon-sdk/source/test/addons/l10n/locale/en.properties
@@ -21,8 +21,18 @@ explicitPlural[other]=other
 unicodeEscape = \u0020\u0040\u0020
 # this string equals to " @ "
 
 # bug 1033309 plurals with multiple placeholders
 first_identifier[one]=first entry is %s and the second one is %s.
 first_identifier=the entries are %s and %s.
 second_identifier[other]=first entry is %s and the second one is %s.
 third_identifier=first entry is %s and the second one is %s.
+
+# bug 824489 allow translation of element attributes
+link-attributes.title=Yes
+link-attributes.alt=Yes
+link-attributes.accesskey=B
+input.placeholder=Yes
+contextitem.label=Yes
+link-attributes.ariaLabel=Yes
+link-attributes.ariaValueText=Value
+link-attributes.ariaMozHint=Hint
--- a/addon-sdk/source/test/addons/l10n/main.js
+++ b/addon-sdk/source/test/addons/l10n/main.js
@@ -100,31 +100,52 @@ exports.testHtmlLocalizationPageWorker =
   let uri = require("sdk/self").data.url("test-localization.html");
   let worker = loader.require("sdk/page-worker").Page({
     contentURL: uri,
     contentScript: "new " + function ContentScriptScope() {
       let nodes = document.body.querySelectorAll("*[data-l10n-id]");
       self.postMessage([nodes[0].innerHTML,
                         nodes[1].innerHTML,
                         nodes[2].innerHTML,
-                        nodes[3].innerHTML]);
+                        nodes[3].innerHTML,
+                        nodes[4].title,
+                        nodes[4].getAttribute("alt"),
+                        nodes[4].getAttribute("accesskey"),
+                        nodes[4].getAttribute("aria-label"),
+                        nodes[4].getAttribute("aria-valuetext"),
+                        nodes[4].getAttribute("aria-moz-hint"),
+                        nodes[5].placeholder,
+                        nodes[6].label]);
     },
     onMessage: function (data) {
       assert.equal(
         data[0],
         "Kept as-is",
         "Nodes with unknown id in .properties are kept 'as-is'"
       );
       assert.equal(data[1], "Yes", "HTML is translated");
       assert.equal(
         data[2],
         "no &lt;b&gt;HTML&lt;/b&gt; injection",
         "Content from .properties is text content; HTML can't be injected."
       );
       assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
+      
+      // Attribute translation tests
+      assert.equal(data[4], "Yes", "Title attributes gets translated.");
+      assert.equal(data[5], "Yes", "Alt attributes gets translated.");
+      assert.equal(data[6], "B", "Accesskey gets translated.");
+      
+      assert.equal(data[7], "Yes", "Aria-Label gets translated.");
+      assert.equal(data[8], "Value", "Aria-valuetext gets translated.");
+      assert.equal(data[9], "Hint", "Aria-moz-hint gets translated.");
+      
+      assert.equal(data[10], "Yes", "Form placeholders are translateable.");
+      
+      assert.equal(data[11], "Yes", "Labels of select options and context menus are translateable.");
 
       done();
     }
   });
 });
 
 exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done) {
   // Ensure initing html component that watch document creations
@@ -139,32 +160,53 @@ exports.testHtmlLocalization = createTes
     onReady: function(tab) {
       tab.attach({
         contentURL: uri,
         contentScript: "new " + function ContentScriptScope() {
           let nodes = document.body.querySelectorAll("*[data-l10n-id]");
           self.postMessage([nodes[0].innerHTML,
                             nodes[1].innerHTML,
                             nodes[2].innerHTML,
-                            nodes[3].innerHTML]);
+                            nodes[3].innerHTML,
+                            nodes[4].title,
+                            nodes[4].getAttribute("alt"),
+                            nodes[4].getAttribute("accesskey"),
+                            nodes[4].getAttribute("aria-label"),
+                            nodes[4].getAttribute("aria-valuetext"),
+                            nodes[4].getAttribute("aria-moz-hint"),
+                            nodes[5].placeholder,
+                            nodes[6].label]);
         },
         onMessage: function (data) {
           assert.equal(
             data[0],
             "Kept as-is",
             "Nodes with unknown id in .properties are kept 'as-is'"
           );
           assert.equal(data[1], "Yes", "HTML is translated");
           assert.equal(
             data[2],
             "no &lt;b&gt;HTML&lt;/b&gt; injection",
             "Content from .properties is text content; HTML can't be injected."
           );
           assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
 
+          // Attribute translation tests
+          assert.equal(data[4], "Yes", "Title attributes gets translated.");
+          assert.equal(data[5], "Yes", "Alt attributes gets translated.");
+          assert.equal(data[6], "B", "Accesskey gets translated.");
+          
+          assert.equal(data[7], "Yes", "Aria-Label gets translated.");
+          assert.equal(data[8], "Value", "Aria-valuetext gets translated.");
+          assert.equal(data[9], "Hint", "Aria-moz-hint gets translated.");
+          
+          assert.equal(data[10], "Yes", "Form placeholders are translateable.");
+          
+          assert.equal(data[11], "Yes", "Labels of select options and context menus are translateable.");
+
           tab.close(done);
         }
       });
     }
   });
 });
 
 exports.testEnUsLocaleName = createTest("en-US", function(assert, loader, done) {
--- a/addon-sdk/source/test/addons/remote/main.js
+++ b/addon-sdk/source/test/addons/remote/main.js
@@ -1,31 +1,44 @@
 /* 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 LOCAL_URI = "about:robots";
-const REMOTE_URI = "about:home";
+const REMOTE_URI = "data:text/html;charset=utf-8,remote";
 
 const { Loader } = require('sdk/test/loader');
 const { getTabs, openTab, closeTab, setTabURL, getBrowserForTab, getURI } = require('sdk/tabs/utils');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { cleanUI } = require("sdk/test/utils");
 const { setTimeout } = require("sdk/timers");
 const { promiseEvent, promiseDOMEvent, promiseEventOnItemAndContainer,
         waitForProcesses, getChildFrameCount, isE10S } = require("./utils");
 const { after } = require('sdk/test/utils');
 const { processID } = require('sdk/system/runtime');
 
 const { set } = require('sdk/preferences/service');
 // The hidden preload browser messes up our frame counts
 set('browser.newtab.preload', false);
 
+function promiseTabFrameAttach(frames) {
+  return new Promise(resolve => {
+    let listener = function(frame, ...args) {
+      if (!frame.isTab)
+        return;
+      frames.off("attach", listener);
+      resolve([frame, ...args]);
+    }
+
+    frames.on("attach", listener);
+  });
+}
+
 // Check that we see a process stop and start
 exports["test process restart"] = function*(assert) {
   if (!isE10S) {
     assert.pass("Skipping test in non-e10s mode");
     return;
   }
 
   let window = getMostRecentBrowserWindow();
@@ -39,30 +52,30 @@ exports["test process restart"] = functi
 
   let remoteProcess = Array.filter(processes, p => p.isRemote)[0];
   let localProcess = Array.filter(processes, p => !p.isRemote)[0];
   let remoteFrame = Array.filter(frames, f => f.process == remoteProcess)[0];
 
   // Switch the remote tab to a local URI which should kill the remote process
 
   let frameDetach = promiseEventOnItemAndContainer(assert, remoteFrame, frames, 'detach');
-  let frameAttach = promiseEvent(frames, 'attach');
+  let frameAttach = promiseTabFrameAttach(frames);
   let processDetach = promiseEventOnItemAndContainer(assert, remoteProcess, processes, 'detach');
   setTabURL(tab, LOCAL_URI);
   // The load should kill the remote frame
   yield frameDetach;
   // And create a new frame in the local process
   let [newFrame] = yield frameAttach;
   assert.equal(newFrame.process, localProcess, "New frame should be in the local process");
   // And kill the process
   yield processDetach;
 
   frameDetach = promiseEventOnItemAndContainer(assert, newFrame, frames, 'detach');
   let processAttach = promiseEvent(processes, 'attach');
-  frameAttach = promiseEvent(frames, 'attach');
+  frameAttach = promiseTabFrameAttach(frames);
   setTabURL(tab, REMOTE_URI);
   // The load should kill the remote frame
   yield frameDetach;
   // And create a new remote process
   [remoteProcess] = yield processAttach;
   assert.ok(remoteProcess.isRemote, "Process should be remote");
   // And create a new frame in the remote process
   [newFrame] = yield frameAttach;
@@ -144,26 +157,26 @@ exports["test frame list"] = function*(a
   assert.equal(tabs.length, 1, "Should have just the one tab to start with");
 
   let loader = new Loader(module);
   let { processes, frames } = yield waitForProcesses(loader);
 
   assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
   assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab1 = openTab(window, LOCAL_URI);
   let [frame1] = yield promise;
   assert.ok(!!frame1, "Should have seen the new frame");
   assert.ok(!frame1.process.isRemote, "Frame should not be remote");
 
   assert.equal(browserFrames(frames), getTabs(window).length, "Should be the right number of browser frames.");
   assert.equal((yield getChildFrameCount(processes)), frames.length, "Child processes should have the right number of frames");
 
-  promise = promiseEvent(frames, 'attach');
+  promise = promiseTabFrameAttach(frames);
   let tab2 = openTab(window, REMOTE_URI);
   let [frame2] = yield promise;
   assert.ok(!!frame2, "Should have seen the new frame");
   if (isE10S)
     assert.ok(frame2.process.isRemote, "Frame should be remote");
   else
     assert.ok(!frame2.process.isRemote, "Frame should not be remote");
 
@@ -251,17 +264,17 @@ exports["test new loader"] = function*(a
 };
 
 // Test that unloading the loader unloads the child instances
 exports["test unload"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab = openTab(window, "data:,<html/>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   assert.ok(!!frame, "Should have seen the new frame");
 
   promise = promiseDOMEvent(browser, 'hashchange');
   frame.port.emit('sdk/test/testunload');
@@ -275,17 +288,17 @@ exports["test unload"] = function*(asser
 }
 
 // Test that unloading the loader causes the child to see frame detach events
 exports["test frame detach on unload"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab = openTab(window, "data:,<html/>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   assert.ok(!!frame, "Should have seen the new frame");
 
   promise = promiseDOMEvent(browser, 'hashchange');
   frame.port.emit('sdk/test/testdetachonunload');
@@ -299,17 +312,17 @@ exports["test frame detach on unload"] =
 }
 
 // Test that DOM event listener on the frame object works
 exports["test frame event listeners"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab = openTab(window, "data:text/html,<html></html>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   assert.ok(!!frame, "Should have seen the new frame");
 
   frame.port.emit('sdk/test/registerframeevent');
   promise = Promise.all([
@@ -334,17 +347,17 @@ exports["test frame event listeners"] = 
 }
 
 // Test that DOM event listener on the frames object works
 exports["test frames event listeners"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
-  let promise = promiseEvent(frames, 'attach');
+  let promise = promiseTabFrameAttach(frames);
   let tab = openTab(window, "data:text/html,<html></html>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   assert.ok(!!frame, "Should have seen the new frame");
 
   frame.port.emit('sdk/test/registerframesevent');
   promise = Promise.all([
@@ -372,18 +385,18 @@ exports["test frames event listeners"] =
 exports["test unload removes frame event listeners"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
   let loader2 = new Loader(module);
   let { frames: frames2 } = yield waitForProcesses(loader2);
 
-  let promise = promiseEvent(frames, 'attach');
-  let promise2 = promiseEvent(frames2, 'attach');
+  let promise = promiseTabFrameAttach(frames);
+  let promise2 = promiseTabFrameAttach(frames2);
   let tab = openTab(window, "data:text/html,<html></html>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   let [frame2] = yield promise2;
   assert.ok(!!frame && !!frame2, "Should have seen the new frame");
 
   frame.port.emit('sdk/test/registerframeevent');
@@ -413,18 +426,18 @@ exports["test unload removes frame event
 exports["test unload removes frames event listeners"] = function*(assert) {
   let window = getMostRecentBrowserWindow();
   let loader = new Loader(module);
   let { frames } = yield waitForProcesses(loader);
 
   let loader2 = new Loader(module);
   let { frames: frames2 } = yield waitForProcesses(loader2);
 
-  let promise = promiseEvent(frames, 'attach');
-  let promise2 = promiseEvent(frames2, 'attach');
+  let promise = promiseTabFrameAttach(frames);
+  let promise2 = promiseTabFrameAttach(frames2);
   let tab = openTab(window, "data:text/html,<html></html>");
   let browser = getBrowserForTab(tab);
   yield promiseDOMEvent(browser, "load", true);
   let [frame] = yield promise;
   let [frame2] = yield promise2;
   assert.ok(!!frame && !!frame2, "Should have seen the new frame");
 
   frame.port.emit('sdk/test/registerframesevent');
--- a/addon-sdk/source/test/jetpack-package.ini
+++ b/addon-sdk/source/test/jetpack-package.ini
@@ -57,16 +57,17 @@ skip-if = true
 [test-dev-panel.js]
 [test-diffpatcher.js]
 [test-dispatcher.js]
 [test-disposable.js]
 [test-dom.js]
 [test-environment.js]
 [test-errors.js]
 [test-event-core.js]
+[test-event-dom.js]
 [test-event-target.js]
 [test-event-utils.js]
 [test-events.js]
 [test-file.js]
 [test-frame-utils.js]
 [test-framescript-manager.js]
 [test-framescript-util.js]
 [test-fs.js]
--- a/addon-sdk/source/test/preferences/no-connections.json
+++ b/addon-sdk/source/test/preferences/no-connections.json
@@ -8,16 +8,17 @@
   "media.gmp-gmpopenh264.autoupdate": false,
   "media.gmp-manager.cert.checkAttributes": false,
   "media.gmp-manager.cert.requireBuiltIn": false,
   "media.gmp-manager.url": "http://localhost/media-dummy/gmpmanager",
   "media.gmp-manager.url.override": "http://localhost/dummy-gmp-manager.xml",
   "browser.aboutHomeSnippets.updateUrl": "https://localhost/snippet-dummy",
   "browser.newtab.url": "about:blank",
   "browser.search.update": false,
+  "browser.search.suggest.enabled": false,
   "browser.safebrowsing.enabled": false,
   "browser.safebrowsing.updateURL": "http://localhost/safebrowsing-dummy/update",
   "browser.safebrowsing.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
   "browser.safebrowsing.reportURL": "http://localhost/safebrowsing-dummy/report",
   "browser.safebrowsing.malware.reportURL": "http://localhost/safebrowsing-dummy/malwarereport",
   "browser.selfsupport.url": "http://localhost/repair-dummy",
   "browser.trackingprotection.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
   "browser.trackingprotection.updateURL": "http://localhost/safebrowsing-dummy/update",
--- a/addon-sdk/source/test/private-browsing/windows.js
+++ b/addon-sdk/source/test/private-browsing/windows.js
@@ -5,16 +5,17 @@
 
 const { onFocus, openDialog, open } = require('sdk/window/utils');
 const { open: openPromise, close, focus, promise } = require('sdk/window/helpers');
 const { isPrivate } = require('sdk/private-browsing');
 const { getMode } = require('sdk/private-browsing/utils');
 const { browserWindows: windows } = require('sdk/windows');
 const { defer } = require('sdk/core/promise');
 const tabs = require('sdk/tabs');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 
 // test openDialog() from window/utils with private option
 // test isActive state in pwpb case
 // test isPrivate on ChromeWindow
 exports.testPerWindowPrivateBrowsingGetter = function*(assert) {
   let win = openDialog({ private: true });
 
   yield promise(win, 'DOMContentLoaded');
@@ -75,37 +76,32 @@ exports.testIsPrivateOnWindowOpenFromPri
       });
 
       return promise;
     }).then(close).
        then(done, assert.fail);
 };
 
 exports.testOpenTabWithPrivateWindow = function*(assert) {
-  let { promise, resolve } = defer();
+  let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
 
-  let window = yield openPromise(null, {
-    features: {
-      private: true,
-      toolbar: true
-    }
-  });
-  yield focus(window);
+  assert.pass("loading new private window");
+
+  yield promise(window, 'load').then(focus);
 
   assert.equal(isPrivate(window), true, 'the focused window is private');
 
-  tabs.open({
+  yield new Promise(resolve => tabs.open({
     url: 'about:blank',
     onOpen: (tab) => {
       assert.equal(isPrivate(tab), false, 'the opened tab is not private');
       tab.close(resolve);
     }
-  });
+  }));
 
-  yield promise;
   yield close(window);
 };
 
 exports.testIsPrivateOnWindowOff = function(assert, done) {
   windows.open({
     onOpen: function(window) {
       assert.equal(isPrivate(window), false, 'isPrivate for a window is false when it should be');
       assert.equal(isPrivate(window.tabs[0]), false, 'isPrivate for a tab is false when it should be');
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/tabs/utils.js
@@ -0,0 +1,24 @@
+/* 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 { openTab: makeTab, getTabContentWindow } = require("sdk/tabs/utils");
+
+function openTab(rawWindow, url) {
+  return new Promise(resolve => {
+    let tab = makeTab(rawWindow, url);
+    let window = getTabContentWindow(tab);
+    if (window.document.readyState == "complete") {
+      return resolve();
+    }
+
+    window.addEventListener("load", function onLoad() {
+      window.removeEventListener("load", onLoad, true);
+      resolve();
+    }, true);
+
+    return null;
+  })
+}
+exports.openTab = openTab;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-event-dom.js
@@ -0,0 +1,92 @@
+/* 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 { openWindow, closeWindow } = require('./util');
+const { Loader } = require('sdk/test/loader');
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { Cc, Ci } = require('chrome');
+const els = Cc["@mozilla.org/eventlistenerservice;1"].
+            getService(Ci.nsIEventListenerService);
+
+function countListeners(target, type) {
+  let listeners = els.getListenerInfoFor(target, {});
+  return listeners.filter(listener => listener.type == type).length;
+}
+
+exports['test window close clears listeners'] = function(assert) {
+  let window = yield openWindow();
+  let loader = Loader(module);
+
+  // Any element will do here
+  let gBrowser = window.gBrowser;
+
+  // Other parts of the app may be listening for this
+  let windowListeners = countListeners(window, "DOMWindowClose");
+
+  // We can assume we're the only ones using the test events
+  assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1");
+  assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
+
+  let { open } = loader.require('sdk/event/dom');
+
+  open(gBrowser, "TestEvent1");
+  assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1,
+               "Should have added a DOMWindowClose listener");
+  assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1");
+  assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
+
+  open(gBrowser, "TestEvent2");
+  assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1,
+               "Should not have added another DOMWindowClose listener");
+  assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1");
+  assert.equal(countListeners(gBrowser, "TestEvent2"), 1, "Should be a listener for test event 2");
+
+  window = yield closeWindow(window);
+
+  assert.equal(countListeners(window, "DOMWindowClose"), windowListeners,
+               "Should have removed a DOMWindowClose listener");
+  assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1");
+  assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
+
+  loader.unload();
+};
+
+exports['test unload clears listeners'] = function(assert) {
+  let window = getMostRecentBrowserWindow();
+  let loader = Loader(module);
+
+  // Any element will do here
+  let gBrowser = window.gBrowser;
+
+  // Other parts of the app may be listening for this
+  let windowListeners = countListeners(window, "DOMWindowClose");
+
+  // We can assume we're the only ones using the test events
+  assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1");
+  assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
+
+  let { open } = loader.require('sdk/event/dom');
+
+  open(gBrowser, "TestEvent1");
+  assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1,
+               "Should have added a DOMWindowClose listener");
+  assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1");
+  assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
+
+  open(gBrowser, "TestEvent2");
+  assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1,
+               "Should not have added another DOMWindowClose listener");
+  assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1");
+  assert.equal(countListeners(gBrowser, "TestEvent2"), 1, "Should be a listener for test event 2");
+
+  loader.unload();
+
+  assert.equal(countListeners(window, "DOMWindowClose"), windowListeners,
+               "Should have removed a DOMWindowClose listener");
+  assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1");
+  assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2");
+};
+
+require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-native-loader.js
+++ b/addon-sdk/source/test/test-native-loader.js
@@ -43,16 +43,24 @@ exports['test nodeResolve'] = function (
     'correctly ignores SDK references in paths');
   resolveTest('fs', './index.js', undefined,
     'correctly ignores built in node modules in paths');
 
   resolveTest('test-add', './node_modules/test-math/index.js',
     './node_modules/test-math/node_modules/test-add/index.js',
     'Dependencies\' dependencies can be found');
 
+  resolveTest('resource://gre/modules/commonjs/sdk/tabs.js', './index.js', undefined,
+              'correctly ignores absolute URIs.');
+
+  resolveTest('../tabs', 'resource://gre/modules/commonjs/sdk/addon/bootstrap.js', undefined,
+              'correctly ignores attempts to resolve from a module at an absolute URI.');
+
+  resolveTest('sdk/tabs', 'resource://gre/modules/commonjs/sdk/addon/bootstrap.js', undefined,
+              'correctly ignores attempts to resolve from a module at an absolute URI.');
 
   function resolveTest (id, requirer, expected, msg) {
     let result = nodeResolve(id, requirer, { manifest: manifest, rootURI: rootURI });
     assert.equal(result, expected, 'nodeResolve ' + id + ' from ' + requirer + ' ' +msg);
   }
 }
 
 /*
--- a/addon-sdk/source/test/test-ui-sidebar.js
+++ b/addon-sdk/source/test/test-ui-sidebar.js
@@ -1486,44 +1486,54 @@ exports.testAttachDoesNotEmitWhenShown =
   sidebar.off('show', onShow);
   sidebar.destroy();
 }
 
 exports.testShowHideRawWindowArg = function*(assert) {
   const { Sidebar } = require('sdk/ui/sidebar');
 
   let testName = 'testShowHideRawWindowArg';
+
+  assert.pass("Creating sidebar");
+
   let sidebar = Sidebar({
     id: testName,
     title: testName,
     url: 'data:text/html;charset=utf-8,' + testName
   });
 
+  assert.pass("Created sidebar");
+
   let mainWindow = getMostRecentBrowserWindow();
   let newWindow = yield windowPromise(mainWindow.OpenBrowserWindow(), 'load');
   assert.pass("Created the new window");
 
   yield focus(newWindow);
   assert.pass("Focused the new window");
 
-  yield focus(mainWindow);
-  assert.pass("Focused the old window");
+  let newWindow2 = yield windowPromise(mainWindow.OpenBrowserWindow(), 'load');
+  assert.pass("Created the second new window");
+
+  yield focus(newWindow2);
+  assert.pass("Focused the second new window");
 
   yield sidebar.show(newWindow);
 
   assert.pass('the sidebar was shown');
   assert.equal(isSidebarShowing(mainWindow), false, 'sidebar is not showing in main window');
+  assert.equal(isSidebarShowing(newWindow2), false, 'sidebar is not showing in second window');
   assert.equal(isSidebarShowing(newWindow), true, 'sidebar is showing in new window');
 
-  assert.ok(isFocused(mainWindow), 'main window is still focused');
+  assert.ok(isFocused(newWindow2), 'main window is still focused');
 
   yield sidebar.hide(newWindow);
 
-  assert.equal(isFocused(mainWindow), true, 'main window is still focused');
+  assert.equal(isFocused(newWindow2), true, 'second window is still focused');
   assert.equal(isSidebarShowing(mainWindow), false, 'sidebar is not showing in main window');
+  assert.equal(isSidebarShowing(newWindow2), false, 'sidebar is not showing in second window');
   assert.equal(isSidebarShowing(newWindow), false, 'sidebar is not showing in new window');
 
   sidebar.destroy();
 }
 
 exports.testShowHideSDKWindowArg = function*(assert) {
   const { Sidebar } = require('sdk/ui/sidebar');
 
--- a/addon-sdk/source/test/util.js
+++ b/addon-sdk/source/test/util.js
@@ -26,17 +26,17 @@ const openWindow = () => {
         }
       }
     }, "browser-delayed-startup-finished", false);
   });
 };
 exports.openWindow = openWindow;
 
 const closeWindow = (window) => {
-  const closed = when(window, "unload", true).then(_target);
+  const closed = when(window, "unload", true).then(_ => window);
   window.close();
   return closed;
 };
 exports.closeWindow = closeWindow;
 
 const openTab = (url, window=getMostRecentBrowserWindow()) => {
   const tab = tabUtils.openTab(window, url);
   const browser = tabUtils.getBrowserForTab(tab);
--- a/addon-sdk/source/test/windows/test-firefox-windows.js
+++ b/addon-sdk/source/test/windows/test-firefox-windows.js
@@ -2,26 +2,29 @@
  * 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, isBrowser, getWindowTitle, isFocused } = require('sdk/window/utils');
-const { open, close, focus } = require('sdk/window/helpers');
+const { open, close, focus, promise: windowPromise } = require('sdk/window/helpers');
 const { browserWindows } = require("sdk/windows");
 const tabs = require("sdk/tabs");
 const winUtils = require("sdk/deprecated/window-utils");
 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");
 const { cleanUI } = require("sdk/test/utils");
 const { after } = require("sdk/test/utils");
+const { merge } = require("sdk/util/object");
+const self = require("sdk/self");
+const { openTab } = require("../tabs/utils");
 
 // 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>",
@@ -54,40 +57,51 @@ exports.testNeWindowIsFocused = function
         assert.ok(isFocused(browserWindows.activeWindow), 'the active window is focused');
         assert.ok(!isFocused(mainWindow), 'the main window is not focused');
         done();
       })
     }
   });
 }
 
-exports.testOpenRelativePathWindow = function(assert, done) {
+exports.testOpenRelativePathWindow = function*(assert) {
   assert.equal(browserWindows.length, 1, "Only one window open");
 
-  const { merge } = require("sdk/util/object");
-  const self = require("sdk/self");
-
   let loader = Loader(module, null, null, {
     modules: {
       "sdk/self": merge({}, self, {
         data: merge({}, self.data, require("./../fixtures"))
       })
     }
   });
+  assert.pass("Created a new loader");
 
-  loader.require("sdk/windows").browserWindows.open({
-    url: "./test.html",
-    onOpen: (window) => {
-      window.tabs.activeTab.once("ready", (tab) => {
-        assert.equal(tab.title, "foo",
-          "tab opened a document with relative path");
-        done();
-      });
-    }
-  })
+  let tabReady = new Promise(resolve => {
+    loader.require("sdk/tabs").on("ready", (tab) => {
+      if (!/test\.html$/.test(tab.url))
+        return;
+      assert.equal(tab.title, "foo",
+        "tab opened a document with relative path");
+      resolve();
+    });
+  });
+
+
+  yield new Promise(resolve => {
+    loader.require("sdk/windows").browserWindows.open({
+      url: "./test.html",
+      onOpen: (window) => {
+        assert.pass("Created a new window");
+        resolve();
+      }
+    })
+  });
+
+  yield tabReady;
+  loader.unload();
 }
 
 exports.testAutomaticDestroy = function(assert, done) {
   let windows = browserWindows;
 
   // Create a second windows instance that we will unload
   let called = false;
   let loader = Loader(module);
@@ -213,83 +227,59 @@ exports.testOnOpenOnCloseListeners = fun
         done();
       });
     }
   });
 };
 
 exports.testActiveWindow = function*(assert) {
   let windows = browserWindows;
-
-  // API window objects
-  let window2, window3;
+  let window = getMostRecentWindow();
 
   // Raw window objects
-  let rawWindow2, rawWindow3;
-
-  yield new Promise(resolve => {
-    windows.open({
-      url: "data:text/html;charset=utf-8,<title>window 2</title>",
-      onOpen: (window) => {
-        assert.pass('window 2 open');
+  let rawWindow2 =  yield windowPromise(window.OpenBrowserWindow(), "load").then(focus);
+  assert.pass("Window 2 was created");
 
-        window.tabs.activeTab.once('ready', () => {
-          assert.pass('window 2 tab activated');
+  // open a tab in window 2
+  yield openTab(rawWindow2, "data:text/html;charset=utf-8,<title>window 2</title>");
 
-          window2 = window;
-          rawWindow2 = viewFor(window);
-
-          assert.equal(rawWindow2.content.document.title, "window 2", "Got correct raw window 2");
-          assert.equal(rawWindow2.document.title, window2.title, "Saw correct title on window 2");
+  assert.equal(rawWindow2.content.document.title, "window 2", "Got correct raw window 2");
+  assert.equal(rawWindow2.document.title, windows[1].title, "Saw correct title on window 2");
 
-          windows.open({
-            url: "data:text/html;charset=utf-8,<title>window 3</title>",
-            onOpen: (window) => {
-              assert.pass('window 3 open');
-
-              window.tabs.activeTab.once('ready', () => {
-                assert.pass('window 3 tab activated');
-
-                window3 = window;
-                rawWindow3 = viewFor(window);
+  let rawWindow3 =  yield windowPromise(window.OpenBrowserWindow(), "load").then(focus);;
+  assert.pass("Window 3 was created");
 
-                assert.equal(rawWindow3.content.document.title, "window 3", "Got correct raw window 3");
-                assert.equal(rawWindow3.document.title, window3.title, "Saw correct title on window 3");
+  // open a tab in window 3
+  yield openTab(rawWindow3, "data:text/html;charset=utf-8,<title>window 3</title>");
 
-                resolve();
-              });
-            }
-          });
-        });
-      }
-    });
-  });
-
-  yield focus(rawWindow3);
+  assert.equal(rawWindow3.content.document.title, "window 3", "Got correct raw window 3");
+  assert.equal(rawWindow3.document.title, windows[2].title, "Saw correct title on window 3");
 
   assert.equal(windows.length, 3, "Correct number of browser windows");
 
   let count = 0;
   for (let window in windows) {
     count++;
   }
   assert.equal(count, 3, "Correct number of windows returned by iterator");
-  assert.equal(windows.activeWindow.title, window3.title, "Correct active window title - 3");
+  assert.equal(windows.activeWindow.title, windows[2].title, "Correct active window title - 3");
+  let window3 = windows[2];
 
   yield focus(rawWindow2);
 
-  assert.equal(windows.activeWindow.title, window2.title, "Correct active window title - 2");
+  assert.equal(windows.activeWindow.title, windows[1].title, "Correct active window title - 2");
+  let window2 = windows[1];
 
   yield new Promise(resolve => {
     onFocus(rawWindow2).then(resolve);
     window2.activate();
     assert.pass("activating window2");
   });
 
-  assert.equal(windows.activeWindow.title, window2.title, "Correct active window - 2");
+  assert.equal(windows.activeWindow.title, windows[1].title, "Correct active window - 2");
 
   yield new Promise(resolve => {
     onFocus(rawWindow3).then(resolve);
     window3.activate();
     assert.pass("activating window3");
   });
 
   assert.equal(windows.activeWindow.title, window3.title, "Correct active window - 3");
@@ -386,31 +376,42 @@ exports.testTrackWindows = function(asse
         actions.push("deactivate " + index)
       }
     }));
   }
   openWindow();
 }
 
 // test that it is not possible to open a private window by default
-exports.testWindowOpenPrivateDefault = function(assert, done) {
-  browserWindows.open({
-    url: 'about:mozilla',
+exports.testWindowOpenPrivateDefault = function*(assert) {
+  const TITLE = "yo";
+  const URL = "data:text/html,<title>" + TITLE + "</title>";
+
+  let tabReady = new Promise(resolve => {
+    tabs.on('ready', function onTabReady(tab) {
+      if (tab.url != URL)
+        return;
+
+      tabs.removeListener('ready', onTabReady);
+      assert.equal(tab.title, TITLE, 'opened correct tab');
+      assert.equal(isPrivate(tab), false, 'tab is not private');
+      resolve();
+    });
+  })
+
+  yield new Promise(resolve => browserWindows.open({
+    url: URL,
     isPrivate: true,
     onOpen: function(window) {
-      let tab = window.tabs[0];
+      assert.pass("the new window was opened");
+      resolve();
+    }
+  }));
 
-      tab.once('ready', function() {
-        assert.equal(tab.url, 'about:mozilla', 'opened correct tab');
-        assert.equal(isPrivate(tab), false, 'tab is not private');
-
-        done();
-      });
-    }
-  });
+  yield tabReady;
 }
 
 // test that it is not possible to find a private window in
 // windows module's iterator
 exports.testWindowIteratorPrivateDefault = function*(assert) {
   assert.equal(browserWindows.length, 1, 'only one window open');
 
   let window = yield open('chrome://browser/content/browser.xul', {
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -485,17 +485,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     handleEvent: function(event) {
       // We only should get "select" events.
       if (event.type != "TabSelect") {
         return;
       }
 
       let wasVisible = false;
       // Hide the infobar from the previous tab.
-      if (event.detail.previousTabfromTab) {
+      if (event.detail.previousTab) {
         wasVisible = this._hideBrowserSharingInfoBar(false, event.detail.previousTab.linkedBrowser);
       }
 
       // We've changed the tab, so get the new window id.
       for (let listener of this._tabChangeListeners) {
         try {
           listener(null, gBrowser.selectedBrowser.outerWindowID);
         } catch (ex) {
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -57,17 +57,17 @@
       <popupnotificationcontent orient="vertical" align="start">
         <separator class="thin"/>
         <label id="pointerLock-cancel">&pointerLock.notification.message;</label>
       </popupnotificationcontent>
     </popupnotification>
 
     <popupnotification id="password-notification" hidden="true">
       <popupnotificationcontent orient="vertical">
-        <textbox id="password-notification-username" disabled="true"/>
+        <textbox id="password-notification-username"/>
         <textbox id="password-notification-password" type="password"
                  disabled="true"/>
       </popupnotificationcontent>
     </popupnotification>
 
 #ifdef E10S_TESTING_ONLY
     <popupnotification id="enable-e10s-notification" hidden="true">
       <popupnotificationcontent orient="vertical"/>
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -94,16 +94,18 @@ support-files =
   zoom_test.html
   test_no_mcb_on_http_site_img.html
   test_no_mcb_on_http_site_img.css
   test_no_mcb_on_http_site_font.html
   test_no_mcb_on_http_site_font.css
   test_no_mcb_on_http_site_font2.html
   test_no_mcb_on_http_site_font2.css
   test_mcb_redirect.html
+  test_mcb_redirect_image.html
+  test_mcb_double_redirect_image.html
   test_mcb_redirect.js
   test_mcb_redirect.sjs
   xul_tooltiptext.xhtml
   file_bug1045809_1.html
   file_bug1045809_2.html
 
 [browser_URLBarSetURI.js]
 skip-if = (os == "linux" || os == "mac") && debug # bug 970052, bug 970053
--- a/browser/base/content/test/general/browser_mcb_redirect.js
+++ b/browser/base/content/test/general/browser_mcb_redirect.js
@@ -1,33 +1,81 @@
 /*
  * Description of the Tests for
  *  - Bug 418354 - Call Mixed content blocking on redirects
  *
+ * Single redirect script tests
  * 1. Load a script over https inside an https page
  *    - the server responds with a 302 redirect to a >> HTTP << script
  *    - the doorhanger should appear!
  *
  * 2. Load a script over https inside an http page
  *    - the server responds with a 302 redirect to a >> HTTP << script
  *    - the doorhanger should not appear!
+ *
+ * Single redirect image tests
+ * 3. Load an image over https inside an https page
+ *    - the server responds with a 302 redirect to a >> HTTP << image
+ *    - the image should not load
+ *
+ * 4. Load an image over https inside an http page
+ *    - the server responds with a 302 redirect to a >> HTTP << image
+ *    - the image should load and get cached
+ *
+ * Single redirect cached image tests
+ * 5. Using offline mode to ensure we hit the cache, load a cached image over
+ *    https inside an http page
+ *    - the server would have responded with a 302 redirect to a >> HTTP <<
+ *      image, but instead we try to use the cached image.
+ *    - the image should load
+ *
+ * 6. Using offline mode to ensure we hit the cache, load a cached image over
+ *    https inside an https page
+ *    - the server would have responded with a 302 redirect to a >> HTTP <<
+ *      image, but instead we try to use the cached image.
+ *    - the image should not load
+ *
+ * Double redirect image test
+ * 7. Load an image over https inside an http page
+ *    - the server responds with a 302 redirect to a >> HTTP << server
+ *    - the HTTP server responds with a 302 redirect to a >> HTTPS << image
+ *    - the image should load and get cached
+ *
+ * Double redirect cached image tests
+ * 8. Using offline mode to ensure we hit the cache, load a cached image over
+ *    https inside an http page
+ *    - the image would have gone through two redirects: HTTPS->HTTP->HTTPS,
+ *      but instead we try to use the cached image.
+ *    - the image should load
+ *
+ * 9. Using offline mode to ensure we hit the cache, load a cached image over
+ *    https inside an https page
+ *    - the image would have gone through two redirects: HTTPS->HTTP->HTTPS,
+ *      but instead we try to use the cached image.
+ *    - the image should not load
  */
 
 const PREF_ACTIVE = "security.mixed_content.block_active_content";
+const PREF_DISPLAY = "security.mixed_content.block_display_content";
 const gHttpsTestRoot = "https://example.com/browser/browser/base/content/test/general/";
 const gHttpTestRoot = "http://example.com/browser/browser/base/content/test/general/";
 
 let origBlockActive;
+let origBlockDisplay;
 var gTestBrowser = null;
 
 //------------------------ Helper Functions ---------------------
 
 registerCleanupFunction(function() {
   // Set preferences back to their original values
   Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive);
+  Services.prefs.setBoolPref(PREF_DISPLAY, origBlockDisplay);
+
+  // Make sure we are online again
+  Services.io.offline = false;
 });
 
 function cleanUpAfterTests() {
   gBrowser.removeCurrentTab();
   window.focus();
   finish();
 }
 
@@ -82,29 +130,185 @@ function checkPopUpNotificationsForTest2
   gTestBrowser.removeEventListener("load", checkPopUpNotificationsForTest2, true);
 
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser.selectedBrowser);
   ok(!notification, "OK: Mixed Content Doorhanger did not appear in 2!");
 
   var expected = "script executed";
   waitForCondition(
     function() content.document.getElementById('mctestdiv').innerHTML == expected,
-    cleanUpAfterTests, "Error: Waited too long for status in Test 2!",
+    test3, "Error: Waited too long for status in Test 2!",
     "OK: Expected result in innerHTML for Test2!");
 }
 
+//------------------------ Test 3 ------------------------------
+// HTTPS page loading insecure image
+function test3() {
+  gTestBrowser.addEventListener("load", checkLoadEventForTest3, true);
+  var url = gHttpsTestRoot + "test_mcb_redirect_image.html"
+  gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest3() {
+  gTestBrowser.removeEventListener("load", checkLoadEventForTest3, true);
+
+  var expected = "image blocked"
+  waitForCondition(
+    function() content.document.getElementById('mctestdiv').innerHTML == expected,
+    test4, "Error: Waited too long for status in Test 3!",
+    "OK: Expected result in innerHTML for Test3!");
+}
+
+//------------------------ Test 4 ------------------------------
+// HTTP page loading insecure image
+function test4() {
+  gTestBrowser.addEventListener("load", checkLoadEventForTest4, true);
+  var url = gHttpTestRoot + "test_mcb_redirect_image.html"
+  gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest4() {
+  gTestBrowser.removeEventListener("load", checkLoadEventForTest4, true);
+
+  var expected = "image loaded"
+  waitForCondition(
+    function() content.document.getElementById('mctestdiv').innerHTML == expected,
+    test5, "Error: Waited too long for status in Test 4!",
+    "OK: Expected result in innerHTML for Test4!");
+}
+
+//------------------------ Test 5 ------------------------------
+// HTTP page laoding insecure cached image
+// Assuming test 4 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test5() {
+  gTestBrowser.addEventListener("load", checkLoadEventForTest5, true);
+  // Go into offline mode
+  Services.io.offline = true;
+  var url = gHttpTestRoot + "test_mcb_redirect_image.html"
+  gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest5() {
+  gTestBrowser.removeEventListener("load", checkLoadEventForTest5, true);
+
+  var expected = "image loaded"
+  waitForCondition(
+    function() content.document.getElementById('mctestdiv').innerHTML == expected,
+    test6, "Error: Waited too long for status in Test 5!",
+    "OK: Expected result in innerHTML for Test5!");
+  // Go back online
+  Services.io.offline = false;
+}
+
+//------------------------ Test 6 ------------------------------
+// HTTPS page loading insecure cached image
+// Assuming test 4 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test6() {
+  gTestBrowser.addEventListener("load", checkLoadEventForTest6, true);
+  // Go into offline mode
+  Services.io.offline = true;
+  var url = gHttpsTestRoot + "test_mcb_redirect_image.html"
+  gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest6() {
+  gTestBrowser.removeEventListener("load", checkLoadEventForTest6, true);
+
+  var expected = "image blocked"
+  waitForCondition(
+    function() content.document.getElementById('mctestdiv').innerHTML == expected,
+    test7, "Error: Waited too long for status in Test 6!",
+    "OK: Expected result in innerHTML for Test6!");
+  // Go back online
+  Services.io.offline = false;
+}
+
+//------------------------ Test 7 ------------------------------
+// HTTP page loading insecure image that went through a double redirect
+function test7() {
+  gTestBrowser.addEventListener("load", checkLoadEventForTest7, true);
+  var url = gHttpTestRoot + "test_mcb_double_redirect_image.html"
+  gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest7() {
+  gTestBrowser.removeEventListener("load", checkLoadEventForTest7, true);
+
+  var expected = "image loaded"
+  waitForCondition(
+    function() content.document.getElementById('mctestdiv').innerHTML == expected,
+    test8, "Error: Waited too long for status in Test 7!",
+    "OK: Expected result in innerHTML for Test7!");
+}
+
+//------------------------ Test 8 ------------------------------
+// HTTP page loading insecure cached image that went through a double redirect
+// Assuming test 7 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test8() {
+  gTestBrowser.addEventListener("load", checkLoadEventForTest8, true);
+  // Go into offline mode
+  Services.io.offline = true;
+  var url = gHttpTestRoot + "test_mcb_double_redirect_image.html"
+  gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest8() {
+  gTestBrowser.removeEventListener("load", checkLoadEventForTest8, true);
+
+  var expected = "image loaded"
+  waitForCondition(
+    function() content.document.getElementById('mctestdiv').innerHTML == expected,
+    test9, "Error: Waited too long for status in Test 8!",
+    "OK: Expected result in innerHTML for Test8!");
+  // Go back online
+  Services.io.offline = false;
+}
+
+//------------------------ Test 9 ------------------------------
+// HTTPS page loading insecure cached image that went through a double redirect
+// Assuming test 7 succeeded, the image has already been loaded once
+// and hence should be cached per the sjs cache-control header
+// Going into offline mode to ensure we are loading from the cache.
+function test9() {
+  gTestBrowser.addEventListener("load", checkLoadEventForTest9, true);
+  // Go into offline mode
+  Services.io.offline = true;
+  var url = gHttpsTestRoot + "test_mcb_double_redirect_image.html"
+  gTestBrowser.contentWindow.location = url;
+}
+
+function checkLoadEventForTest9() {
+  gTestBrowser.removeEventListener("load", checkLoadEventForTest9, true);
+
+  var expected = "image blocked"
+  waitForCondition(
+    function() content.document.getElementById('mctestdiv').innerHTML == expected,
+    cleanUpAfterTests, "Error: Waited too long for status in Test 9!",
+    "OK: Expected result in innerHTML for Test9!");
+  // Go back online
+  Services.io.offline = false;
+}
+
 //------------------------ SETUP ------------------------------
 
 function test() {
   // Performing async calls, e.g. 'onload', we have to wait till all of them finished
   waitForExplicitFinish();
 
   // Store original preferences so we can restore settings after testing
   origBlockActive = Services.prefs.getBoolPref(PREF_ACTIVE);
+  origBlockDisplay = Services.prefs.getBoolPref(PREF_DISPLAY);
   Services.prefs.setBoolPref(PREF_ACTIVE, true);
+  Services.prefs.setBoolPref(PREF_DISPLAY, true);
 
   var newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
   gTestBrowser = gBrowser.selectedBrowser;
   newTab.linkedBrowser.stop();
 
   executeSoon(test1);
 }
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/test_mcb_double_redirect_image.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+  Test 7-9 for Bug 1082837 - See file browser_mcb_redirect.js for description.
+  https://bugzilla.mozilla.org/show_bug.cgi?id=1082837
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Bug 1082837</title>
+  <script>
+    function image_loaded() {
+      document.getElementById("mctestdiv").innerHTML = "image loaded";
+    }
+    function image_blocked() {
+      document.getElementById("mctestdiv").innerHTML = "image blocked";
+    }
+  </script>
+</head>
+<body>
+  <div id="mctestdiv"></div>
+  <img src="https://example.com/browser/browser/base/content/test/general/test_mcb_redirect.sjs?image_redirect_http_sjs" onload="image_loaded()" onerror="image_blocked()" ></image>
+</body>
+</html>
--- a/browser/base/content/test/general/test_mcb_redirect.html
+++ b/browser/base/content/test/general/test_mcb_redirect.html
@@ -5,11 +5,11 @@
   https://bugzilla.mozilla.org/show_bug.cgi?id=418354
 -->
 <head>
   <meta charset="utf-8">
   <title>Bug 418354</title>
 </head>
 <body>
   <div id="mctestdiv">script blocked</div>
-  <script src="https://example.com/browser/browser/base/content/test/general/test_mcb_redirect.sjs" ></script>
+  <script src="https://example.com/browser/browser/base/content/test/general/test_mcb_redirect.sjs?script" ></script>
 </body>
 </html>
--- a/browser/base/content/test/general/test_mcb_redirect.sjs
+++ b/browser/base/content/test/general/test_mcb_redirect.sjs
@@ -1,11 +1,22 @@
 function handleRequest(request, response) {
-  var page = "<!DOCTYPE html><html><body>bug 418354</body></html>";
+  var page = "<!DOCTYPE html><html><body>bug 418354 and bug 1082837</body></html>";
 
-  var redirect = "http://example.com/browser/browser/base/content/test/general/test_mcb_redirect.js";
+  if (request.queryString === "script") { 
+    var redirect = "http://example.com/browser/browser/base/content/test/general/test_mcb_redirect.js";
+    response.setHeader("Cache-Control", "no-cache", false);
+  } else if (request.queryString === "image_http") {
+    var redirect = "http://example.com/tests/image/test/mochitest/blue.png";
+    response.setHeader("Cache-Control", "max-age=3600", false);
+  } else if (request.queryString === "image_redirect_http_sjs") {
+    var redirect = "http://example.com/browser/browser/base/content/test/general/test_mcb_redirect.sjs?image_redirect_https";
+    response.setHeader("Cache-Control", "max-age=3600", false);
+  } else if (request.queryString === "image_redirect_https") {
+    var redirect = "https://example.com/tests/image/test/mochitest/blue.png";
+    response.setHeader("Cache-Control", "max-age=3600", false);
+  }
 
-  response.setHeader("Cache-Control", "no-cache", false);
   response.setHeader("Content-Type", "text/html", false);
   response.setStatusLine(request.httpVersion, "302", "Found");
   response.setHeader("Location", redirect, false);
   response.write(page);
 }
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/test_mcb_redirect_image.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+  Test 3-6 for Bug 1082837 - See file browser_mcb_redirect.js for description.
+  https://bugzilla.mozilla.org/show_bug.cgi?id=1082837
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Bug 1082837</title>
+  <script>
+    function image_loaded() {
+      document.getElementById("mctestdiv").innerHTML = "image loaded";
+    }
+    function image_blocked() {
+      document.getElementById("mctestdiv").innerHTML = "image blocked";
+    }
+  </script>
+</head>
+<body>
+  <div id="mctestdiv"></div>
+  <img src="https://example.com/browser/browser/base/content/test/general/test_mcb_redirect.sjs?image_http" onload="image_loaded()" onerror="image_blocked()" ></image>
+</body>
+</html>
--- a/browser/base/content/test/referrer/browser_referrer_middle_click.js
+++ b/browser/base/content/test/referrer/browser_referrer_middle_click.js
@@ -9,11 +9,11 @@ function startMiddleClickTestCase(aTestN
     checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
                                   startMiddleClickTestCase);
   });
 
   clickTheLink(gTestWindow, "testlink", {button: 1});
 }
 
 function test() {
-  requestLongerTimeout(5);  // slowwww shutdown on e10s
+  requestLongerTimeout(10);  // slowwww shutdown on e10s
   startReferrerTest(startMiddleClickTestCase);
 }
--- a/browser/base/content/test/referrer/browser_referrer_open_link_in_private.js
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_private.js
@@ -12,11 +12,11 @@ function startNewPrivateWindowTestCase(a
       });
     });
 
     doContextMenuCommand(gTestWindow, aContextMenu, "context-openlinkprivate");
   });
 }
 
 function test() {
-  requestLongerTimeout(5);  // slowwww shutdown on e10s
+  requestLongerTimeout(10);  // slowwww shutdown on e10s
   startReferrerTest(startNewPrivateWindowTestCase);
 }
--- a/browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_tab.js
@@ -11,11 +11,11 @@ function startNewTabTestCase(aTestNumber
                                     startNewTabTestCase);
     });
 
     doContextMenuCommand(gTestWindow, aContextMenu, "context-openlinkintab");
   });
 }
 
 function test() {
-  requestLongerTimeout(5);  // slowwww shutdown on e10s
+  requestLongerTimeout(10);  // slowwww shutdown on e10s
   startReferrerTest(startNewTabTestCase);
 }
--- a/browser/base/content/test/referrer/browser_referrer_open_link_in_window.js
+++ b/browser/base/content/test/referrer/browser_referrer_open_link_in_window.js
@@ -12,11 +12,11 @@ function startNewWindowTestCase(aTestNum
       });
     });
 
     doContextMenuCommand(gTestWindow, aContextMenu, "context-openlink");
   });
 }
 
 function test() {
-  requestLongerTimeout(5);  // slowwww shutdown on e10s
+  requestLongerTimeout(10);  // slowwww shutdown on e10s
   startReferrerTest(startNewWindowTestCase);
 }
--- a/browser/base/content/test/referrer/browser_referrer_simple_click.js
+++ b/browser/base/content/test/referrer/browser_referrer_simple_click.js
@@ -8,11 +8,11 @@ function startSimpleClickTestCase(aTestN
     checkReferrerAndStartNextTest(aTestNumber, null, null,
                                   startSimpleClickTestCase);
   });
 
   clickTheLink(gTestWindow, "testlink", {});
 };
 
 function test() {
-  requestLongerTimeout(5);  // slowwww shutdown on e10s
+  requestLongerTimeout(10);  // slowwww shutdown on e10s
   startReferrerTest(startSimpleClickTestCase);
 }
--- a/browser/components/preferences/in-content/content.xul
+++ b/browser/components/preferences/in-content/content.xul
@@ -85,17 +85,17 @@
 </groupbox>
 
 <!-- Fonts and Colors -->
 <groupbox id="fontsGroup" data-category="paneContent" hidden="true">
   <caption><label>&fontsAndColors.label;</label></caption>
   <hbox align="center">
     <label control="defaultFont" accesskey="&defaultFont.accesskey;">&defaultFont.label;</label>
     <menulist id="defaultFont" flex="1"/>
-    <label control="defaultFontSize" accesskey="&defaultSize.accesskey;">&defaultSize.label;</label>
+    <label id="defaultFontSizeLabel" control="defaultFontSize" accesskey="&defaultSize.accesskey;">&defaultSize.label;</label>
     <menulist id="defaultFontSize">
       <menupopup>
         <menuitem value="9" label="9"/>
         <menuitem value="10" label="10"/>
         <menuitem value="11" label="11"/>
         <menuitem value="12" label="12"/>
         <menuitem value="13" label="13"/>
         <menuitem value="14" label="14"/>
--- a/browser/components/sessionstore/SessionMigration.jsm
+++ b/browser/components/sessionstore/SessionMigration.jsm
@@ -67,24 +67,19 @@ let SessionMigrationInternal = {
             win.extData[k] = oldWin.extData[k];
           }
         }
       }
       win.selected = oldWin.selected;
       win._closedTabs = [];
       return win;
     });
-    let wrappedState = {
-      url: "about:welcomeback",
-      formdata: {
-        id: {"sessionData": state},
-        xpath: {}
-      }
-    };
-    return {windows: [{tabs: [{entries: [wrappedState]}]}]};
+    let url = "about:welcomeback";
+    let formdata = {id: {sessionData: state}, url};
+    return {windows: [{tabs: [{entries: [{url}], formdata}]}]};
   },
   /**
    * Asynchronously read session restore state (JSON) from a path
    */
   readState: function(aPath) {
     return Task.spawn(function() {
       let bytes = yield OS.File.read(aPath);
       let text = gDecoder.decode(bytes);
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -465,24 +465,19 @@ let SessionStoreInternal = {
           LastSession.setState(state.lastSessionState);
 
           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: {
-                  id: { "sessionData": state },
-                  xpath: {}
-                }
-              };
-              state = { windows: [{ tabs: [{ entries: [pageData] }] }] };
+              let url = "about:sessionrestore";
+              let formdata = {id: {sessionData: state}, url};
+              state = { windows: [{ tabs: [{ entries: [{url}], formdata }] }] };
             } else if (this._hasSingleTabWithURL(state.windows,
                                                  "about:welcomeback")) {
               // On a single about:welcomeback URL that crashed, replace about:welcomeback
               // with about:sessionrestore, to make clear to the user that we crashed.
               state.windows[0].tabs[0].entries[0].url = "about:sessionrestore";
             }
           }
 
--- a/browser/components/uitour/test/browser_no_tabs.js
+++ b/browser/components/uitour/test/browser_no_tabs.js
@@ -1,62 +1,54 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+let HiddenFrame = Cu.import("resource:///modules/HiddenFrame.jsm", {}).HiddenFrame;
+
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
  * Create a frame in the |hiddenDOMWindow| to host a |browser|, then load the URL in the
  * latter.
  *
  * @param aURL
  *        The URL to open in the browser.
  **/
 function createHiddenBrowser(aURL) {
-  let deferred = Promise.defer();
-  let hiddenDoc = Services.appShell.hiddenDOMWindow.document;
-
-  // Create a HTML iframe with a chrome URL, then this can host the browser.
-  let iframe = hiddenDoc.createElementNS(HTML_NS, "iframe");
-  iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
-  iframe.addEventListener("load", function onLoad() {
-    iframe.removeEventListener("load", onLoad, true);
+  let frame = new HiddenFrame();
+  return new Promise(resolve =>
+    frame.get().then(aFrame => {
+      let doc = aFrame.document;
+      let browser = doc.createElementNS(XUL_NS, "browser");
+      browser.setAttribute("type", "content");
+      browser.setAttribute("disableglobalhistory", "true");
+      browser.setAttribute("src", aURL);
 
-    let browser = iframe.contentDocument.createElementNS(XUL_NS, "browser");
-    browser.setAttribute("type", "content");
-    browser.setAttribute("disableglobalhistory", "true");
-    browser.setAttribute("src", aURL);
-
-    iframe.contentDocument.documentElement.appendChild(browser);
-    deferred.resolve({frame: iframe, browser: browser});
-  }, true);
-
-  hiddenDoc.documentElement.appendChild(iframe);
-  return deferred.promise;
-};
+      doc.documentElement.appendChild(browser);
+      resolve({frame: frame, browser: browser});
+    }));
+}
 
 /**
- * Remove the browser and the iframe.
+ * Remove the browser and the HiddenFrame.
  *
  * @param aFrame
- *        The iframe to dismiss.
+ *        The HiddenFrame to dismiss.
  * @param aBrowser
  *        The browser to dismiss.
  */
 function destroyHiddenBrowser(aFrame, aBrowser) {
   // Dispose of the hidden browser.
   aBrowser.remove();
 
   // Take care of the frame holding our invisible browser.
-  if (!Cu.isDeadWrapper(aFrame)) {
-    aFrame.remove();
-  }
+  aFrame.destroy();
 };
 
 /**
  * Test that UITour works when called when no tabs are available (e.g., when using windowless
  * browsers).
  */
 add_task(function* test_windowless_UITour(){
   // Get the URL for the test page.
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -491,20 +491,32 @@ ThreadState.prototype = {
 
   /**
    * Update the UI after a thread state change.
    */
   _update: function(aEvent, aPacket) {
     // Ignore "interrupted" events, to avoid UI flicker. These are generated
     // by the slow script dialog and internal events such as setting
     // breakpoints. Pressing the resume button does need to be shown, though.
-    if (aEvent == "paused" &&
-        aPacket.why.type == "interrupted" &&
-        !this.interruptedByResumeButton) {
-      return;
+    if (aEvent == "paused") {
+      if (aPacket.why.type == "interrupted" &&
+          !this.interruptedByResumeButton) {
+        return;
+      } else if (aPacket.why.type == "breakpointConditionThrown" && aPacket.why.message) {
+        let where = aPacket.frame.where;
+        let aLocation = {
+          line: where.line,
+          column: where.column,
+          actor: where.source ? where.source.actor : null
+        };
+        DebuggerView.Sources.showBreakpointConditionThrownMessage(
+          aLocation,
+          aPacket.why.message
+        );
+      }
     }
 
     this.interruptedByResumeButton = false;
     DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state);
 
     if (gTarget && (aEvent == "paused" || aEvent == "resumed")) {
       gTarget.emit("thread-" + aEvent);
     }
@@ -585,16 +597,20 @@ StackFrames.prototype = {
    *        The response packet.
    */
   _onPaused: function(aEvent, aPacket) {
     switch (aPacket.why.type) {
       // If paused by a breakpoint, store the breakpoint location.
       case "breakpoint":
         this._currentBreakpointLocation = aPacket.frame.where;
         break;
+      case "breakpointConditionThrown":
+        this._currentBreakpointLocation = aPacket.frame.where;
+        this._conditionThrowMessage = aPacket.why.message;
+        break;
       // If paused by a client evaluation, store the evaluated value.
       case "clientEvaluated":
         this._currentEvaluation = aPacket.why.frameFinished;
         break;
       // If paused by an exception, store the exception value.
       case "exception":
         this._currentException = aPacket.why.exception;
         break;
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -446,16 +446,29 @@ SourcesView.prototype = Heritage.extend(
    * Unhighlights the current breakpoint in this sources container.
    */
   unhighlightBreakpoint: function() {
     this._hideConditionalPopup();
     this._unselectBreakpoint();
   },
 
   /**
+   * Display the message thrown on breakpoint condition
+   */
+  showBreakpointConditionThrownMessage: function(aLocation, aMessage = "") {
+    let breakpointItem = this.getBreakpoint(aLocation);
+    if (!breakpointItem) {
+      return;
+    }
+    let attachment = breakpointItem.attachment;
+    attachment.view.container.classList.add("dbg-breakpoint-condition-thrown");
+    attachment.view.message.setAttribute("value", aMessage);
+  },
+
+  /**
    * Update the checked/unchecked and enabled/disabled states of the buttons in
    * the sources toolbar based on the currently selected source's state.
    */
   updateToolbarButtonsState: function() {
     const { source } = this.selectedItem.attachment;
     const sourceClient = gThreadClient.source(source);
 
     if (sourceClient.isBlackBoxed) {
@@ -684,22 +697,23 @@ SourcesView.prototype = Heritage.extend(
   /**
    * Customization function for creating a breakpoint item's UI.
    *
    * @param object aOptions
    *        A couple of options or flags supported by this operation:
    *          - location: the breakpoint's source location and line number
    *          - disabled: the breakpoint's disabled state, boolean
    *          - text: the breakpoint's line text to be displayed
+   *          - message: thrown string when the breakpoint condition throws,
    * @return object
    *         An object containing the breakpoint container, checkbox,
    *         line number and line text nodes.
    */
   _createBreakpointView: function(aOptions) {
-    let { location, disabled, text } = aOptions;
+    let { location, disabled, text, message } = aOptions;
     let identifier = DebuggerController.Breakpoints.getIdentifier(location);
 
     let checkbox = document.createElement("checkbox");
     checkbox.setAttribute("checked", !disabled);
     checkbox.className = "dbg-breakpoint-checkbox";
 
     let lineNumberNode = document.createElement("label");
     lineNumberNode.className = "plain dbg-breakpoint-line";
@@ -709,35 +723,55 @@ SourcesView.prototype = Heritage.extend(
     lineTextNode.className = "plain dbg-breakpoint-text";
     lineTextNode.setAttribute("value", text);
     lineTextNode.setAttribute("crop", "end");
     lineTextNode.setAttribute("flex", "1");
 
     let tooltip = text ? text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH) : "";
     lineTextNode.setAttribute("tooltiptext", tooltip);
 
+    let thrownNode = document.createElement("label");
+    thrownNode.className = "plain dbg-breakpoint-condition-thrown-message dbg-breakpoint-text";
+    thrownNode.setAttribute("value", message);
+    thrownNode.setAttribute("crop", "end");
+    thrownNode.setAttribute("flex", "1");
+
+    let bpLineContainer = document.createElement("hbox");
+    bpLineContainer.className = "plain dbg-breakpoint-line-container";
+    bpLineContainer.setAttribute("flex", "1");
+
+    bpLineContainer.appendChild(lineNumberNode);
+    bpLineContainer.appendChild(lineTextNode);
+
+    let bpDetailContainer = document.createElement("vbox");
+    bpDetailContainer.className = "plain dbg-breakpoint-detail-container";
+    bpDetailContainer.setAttribute("flex", "1");
+
+    bpDetailContainer.appendChild(bpLineContainer);
+    bpDetailContainer.appendChild(thrownNode);
+
     let container = document.createElement("hbox");
     container.id = "breakpoint-" + identifier;
     container.className = "dbg-breakpoint side-menu-widget-item-other";
     container.classList.add("devtools-monospace");
     container.setAttribute("align", "center");
     container.setAttribute("flex", "1");
 
     container.addEventListener("click", this._onBreakpointClick, false);
     checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false);
 
     container.appendChild(checkbox);
-    container.appendChild(lineNumberNode);
-    container.appendChild(lineTextNode);
+    container.appendChild(bpDetailContainer);
 
     return {
       container: container,
       checkbox: checkbox,
       lineNumber: lineNumberNode,
-      lineText: lineTextNode
+      lineText: lineTextNode,
+      message: thrownNode
     };
   },
 
   /**
    * Creates a context menu for a breakpoint element.
    *
    * @param object aOptions
    *        A couple of options or flags supported by this operation:
--- a/browser/devtools/debugger/debugger.css
+++ b/browser/devtools/debugger/debugger.css
@@ -43,8 +43,18 @@
 #body[layout=horizontal] #vertical-layout-splitter,
 #body[layout=horizontal] #vertical-layout-panes-container {
   display: none;
 }
 
 #body[layout=vertical] #stackframes {
   visibility: hidden;
 }
+
+#source-progress-container {
+  display: flex;
+  flex-flow: column;
+  justify-content: center;
+}
+
+#source-progress {
+  flex: none;
+}
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -10,16 +10,17 @@
 <?xml-stylesheet href="chrome://browser/skin/devtools/debugger.css" type="text/css"?>
 <!DOCTYPE window [
   <!ENTITY % debuggerDTD SYSTEM "chrome://browser/locale/devtools/debugger.dtd">
   %debuggerDTD;
 ]>
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
         macanimationtype="document"
         fullscreenbutton="true"
         screenX="4" screenY="4"
         width="960" height="480"
         persist="screenX screenY width height sizemode">
 
   <script type="application/javascript;version=1.8"
           src="chrome://browser/content/devtools/theme-switching.js"/>
@@ -416,22 +417,22 @@
           <description id="black-boxed-message-label">
             &debuggerUI.blackBoxMessage.label;
           </description>
           <button id="black-boxed-message-button"
                   class="devtools-toolbarbutton"
                   label="&debuggerUI.blackBoxMessage.unBlackBoxButton;"
                   command="unBlackBoxCommand"/>
         </vbox>
-        <vbox id="source-progress-container"
-              align="center"
-              pack="center">
-          <progressmeter id="source-progress"
-                         mode="undetermined"/>
-        </vbox>
+        <html:div id="source-progress-container"
+                  align="center">
+          <html:div id="hbox">
+            <html:progress id="source-progress"></html:progress>
+          </html:div>
+        </html:div>
       </deck>
       <splitter id="editor-and-instruments-splitter"
                 class="devtools-side-splitter"/>
       <tabbox id="instruments-pane"
               class="devtools-sidebar-tabs"
               hidden="true">
         <tabs>
           <tab id="variables-tab" label="&debuggerUI.tabs.variables;"/>
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -138,16 +138,18 @@ skip-if = e10s # TODO
 [browser_dbg_break-on-dom-event-03.js]
 skip-if = e10s # TODO
 [browser_dbg_breakpoints-actual-location.js]
 [browser_dbg_breakpoints-actual-location2.js]
 [browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js]
 skip-if = e10s # Bug 1093535
 [browser_dbg_breakpoints-button-01.js]
 [browser_dbg_breakpoints-button-02.js]
+[browser_dbg_breakpoints-condition-thrown-message.js]
+skip-if = e10s && debug
 [browser_dbg_breakpoints-contextmenu-add.js]
 [browser_dbg_breakpoints-contextmenu.js]
 [browser_dbg_breakpoints-disabled-reload.js]
 skip-if = e10s # Bug 1093535
 [browser_dbg_breakpoints-editor.js]
 skip-if = e10s && debug
 [browser_dbg_breakpoints-eval.js]
 skip-if = e10s && debug
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_breakpoints-condition-thrown-message.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the message which breakpoint condition throws
+ * could be displayed on UI correctly
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_conditional-breakpoints.html";
+
+function test() {
+  let gTab, gPanel, gDebugger, gEditor;
+  let gSources, gLocation;
+
+  initDebugger(TAB_URL).then(([aTab,, aPanel]) => {
+    gTab = aTab;
+    gPanel = aPanel;
+    gDebugger = gPanel.panelWin;
+    gEditor = gDebugger.DebuggerView.editor;
+    gSources = gDebugger.DebuggerView.Sources;
+
+    waitForSourceAndCaretAndScopes(gPanel, ".html", 17)
+      .then(addBreakpoints)
+      .then(() => resumeAndTestThrownMessage(18))
+      .then(() => resumeAndTestNoThrownMessage(19))
+      .then(() => resumeAndTestThrownMessage(22))
+      .then(() => resumeAndFinishTest())
+      .then(() => closeDebuggerAndFinish(gPanel))
+      .then(null, aError => {
+        ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+      });
+
+    callInTab(gTab, "ermahgerd");
+  });
+
+  function resumeAndTestThrownMessage(aLine) {
+    EventUtils.sendMouseEvent({ type: "mousedown" },
+      gDebugger.document.getElementById("resume"),
+      gDebugger);
+
+    let finished = waitForCaretUpdated(gPanel, aLine).then(() => {
+      //test that the thrown message is correctly shown
+      let attachment = gSources.getBreakpoint({ actor: gSources.values[0], line: aLine}).attachment;
+      ok(attachment.view.container.classList.contains('dbg-breakpoint-condition-thrown'),
+        "Message on line " + aLine + " should be shown when condition throws.");
+    });
+
+    return finished;
+  }
+
+  function resumeAndTestNoThrownMessage(aLine) {
+    EventUtils.sendMouseEvent({ type: "mousedown" },
+      gDebugger.document.getElementById("resume"),
+      gDebugger);
+
+    let finished = waitForCaretUpdated(gPanel, aLine).then(() => {
+      //test that the thrown message is correctly shown
+      let attachment = gSources.getBreakpoint({ actor: gSources.values[0], line: aLine}).attachment;
+      ok(!attachment.view.container.classList.contains("dbg-breakpoint-condition-thrown"),
+        "Message on line " + aLine + " should be hidden if condition doesn't throw.");
+    });
+    return finished;
+  }
+
+  function resumeAndFinishTest() {
+    let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED)
+
+    gDebugger.gThreadClient.resume();
+
+    return finished;
+  }
+
+  function addBreakpoints() {
+    return promise.resolve(null)
+      .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue,
+                                         line: 18,
+                                         condition: " 1a"}))
+      .then(() => initialCheck(18))
+      .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue,
+                                         line: 19,
+                                         condition: "true"}))
+      .then(() => initialCheck(19))
+      .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue,
+                                         line: 20,
+                                         condition: "false"}))
+      .then(() => initialCheck(20))
+      .then(() => gPanel.addBreakpoint({ actor: gSources.selectedValue,
+                                         line: 22,
+                                         condition: "randomVar"}))
+      .then(() => initialCheck(22));
+  }
+
+  function initialCheck(aCaretLine) {
+    let bp = gSources.getBreakpoint({ actor: gSources.values[0], line: aCaretLine})
+    let attachment = bp.attachment;
+    ok(attachment,
+      "There should be an item for line " + aCaretLine + " in the sources pane.");
+
+    let thrownNode = attachment.view.container.querySelector(".dbg-breakpoint-condition-thrown-message");
+    ok(thrownNode,
+      "The breakpoint item should contain a thrown message node.")
+
+    ok(!attachment.view.container.classList.contains("dbg-breakpoint-condition-thrown"),
+      "The thrown message on line " + aCaretLine + " should be hidden when condition has not been evaluated.")
+  }
+}
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -430,21 +430,18 @@ function Rule(aElementStyle, aOptions) {
   this.matchedSelectors = aOptions.matchedSelectors || [];
   this.pseudoElement = aOptions.pseudoElement || "";
 
   this.isSystem = aOptions.isSystem;
   this.inherited = aOptions.inherited || null;
   this.keyframes = aOptions.keyframes || null;
   this._modificationDepth = 0;
 
-  if (this.domRule) {
-    let parentRule = this.domRule.parentRule;
-    if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) {
-      this.mediaText = parentRule.mediaText;
-    }
+  if (this.domRule && this.domRule.mediaText) {
+    this.mediaText = this.domRule.mediaText;
   }
 
   // Populate the text properties with the style's current cssText
   // value, and add in any disabled properties from the store.
   this.textProps = this._getTextProperties();
   this.textProps = this.textProps.concat(this._getDisabledProperties());
 }
 
@@ -502,17 +499,17 @@ Rule.prototype = {
   get sheet() {
     return this.domRule ? this.domRule.parentStyleSheet : null;
   },
 
   /**
    * The rule's line within a stylesheet
    */
   get ruleLine() {
-    return this.domRule ? this.domRule.line : null;
+    return this.domRule ? this.domRule.line : "";
   },
 
   /**
    * The rule's column within a stylesheet
    */
   get ruleColumn() {
     return this.domRule ? this.domRule.column : null;
   },
@@ -524,20 +521,22 @@ Rule.prototype = {
    * @return {Promise}
    *         Promise which resolves with location as an object containing
    *         both the full and short version of the source string.
    */
   getOriginalSourceStrings: function() {
     if (this._originalSourceStrings) {
       return promise.resolve(this._originalSourceStrings);
     }
-    return this.domRule.getOriginalLocation().then(({href, line}) => {
+    return this.domRule.getOriginalLocation().then(({href, line, mediaText}) => {
+      let mediaString = mediaText ? " @" + mediaText : "";
+
       let sourceStrings = {
-        full: href + ":" + line,
-        short: CssLogic.shortSource({href: href}) + ":" + line
+        full: (href || CssLogic.l10n("rule.sourceInline")) + ":" + line + mediaString,
+        short: CssLogic.shortSource({href: href}) + ":" + line + mediaString
       };
 
       this._originalSourceStrings = sourceStrings;
       return sourceStrings;
     }, console.error);
   },
 
   /**
--- a/browser/devtools/styleinspector/test/browser_ruleview_content_01.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_content_01.js
@@ -7,18 +7,20 @@
 // Test that the rule-view content is correct
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8,browser_ruleview_content.js");
   let {toolbox, inspector, view} = yield openRuleView();
 
   info("Creating the test document");
   let style = "" +
-    "#testid {" +
-    "  background-color: blue;" +
+    "@media screen and (min-width: 10px) {" +
+    "  #testid {" +
+    "    background-color: blue;" +
+    "  }" +
     "}" +
     ".testclass, .unmatched {" +
     "  background-color: green;" +
     "}";
   let styleNode = addStyle(content.document, style);
   content.document.body.innerHTML = "<div id='testid' class='testclass'>Styled Node</div>" +
                                     "<div id='testid2'>Styled Node</div>";
 
@@ -30,14 +32,23 @@ function* testContentAfterNodeSelection(
   is(ruleView.element.querySelectorAll("#noResults").length, 0,
     "After a highlight, no longer has a no-results element.");
 
   yield clearCurrentNodeSelection(inspector)
   is(ruleView.element.querySelectorAll("#noResults").length, 1,
     "After highlighting null, has a no-results element again.");
 
   yield selectNode("#testid", inspector);
+
+  let linkText = getRuleViewLinkTextByIndex(ruleView, 1);
+  is(linkText, "inline:1 @screen and (min-width: 10px)",
+    "link text at index 1 contains media query text.");
+
+  linkText = getRuleViewLinkTextByIndex(ruleView, 2);
+  is(linkText, "inline:1",
+    "link text at index 2 contains no media query text.");
+
   let classEditor = getRuleViewRuleEditor(ruleView, 2);
   is(classEditor.selectorText.querySelector(".ruleview-selector-matched").textContent,
     ".testclass", ".textclass should be matched.");
   is(classEditor.selectorText.querySelector(".ruleview-selector-unmatched").textContent,
     ".unmatched", ".unmatched should not be matched.");
 }
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -689,16 +689,27 @@ let simulateColorPickerChange = Task.asy
  * @return {DOMNode} The link if any at this index
  */
 function getRuleViewLinkByIndex(view, index) {
   let links = view.doc.querySelectorAll(".ruleview-rule-source");
   return links[index];
 }
 
 /**
+ * Get rule-link text from the rule-view given its index
+ * @param {CssRuleView} view The instance of the rule-view panel
+ * @param {Number} index The index of the link to get
+ * @return {String} The string at this index
+ */
+function getRuleViewLinkTextByIndex(view, index) {
+  let link = getRuleViewLinkByIndex(view, index);
+  return link.querySelector(".source-link-label").value;
+}
+
+/**
  * Get the rule editor from the rule-view given its index
  * @param {CssRuleView} view The instance of the rule-view panel
  * @param {Number} childrenIndex The children index of the element to get
  * @param {Number} nodeIndex The child node index of the element to get
  * @return {DOMNode} The rule editor if any at this index
  */
 function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) {
   return nodeIndex !== undefined ?
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -297,19 +297,18 @@ let UI = {
     let progress = document.querySelector("#action-busy-determined");
     progress.mode = "undetermined";
     win.classList.add("busy-determined");
     win.classList.remove("busy-undetermined");
     return busy;
   },
 
   busyUntil: function(promise, operationDescription) {
-    // Freeze the UI until the promise is resolved. A 30s timeout
-    // will unfreeze the UI, just in case the promise never gets
-    // resolved.
+    // Freeze the UI until the promise is resolved. A timeout will unfreeze the
+    // UI, just in case the promise never gets resolved.
     this._busyPromise = promise;
     this._busyOperationDescription = operationDescription;
     this.setupBusyTimeout();
     this.busy();
     promise.then(() => {
       this.cancelBusyTimeout();
       this.unbusy();
     }, (e) => {
@@ -464,17 +463,23 @@ let UI = {
     let name = runtime.name;
     let promise = AppManager.connectToRuntime(runtime);
     promise.then(() => this.initConnectionTelemetry())
            .catch(() => {
              // Empty rejection handler to silence uncaught rejection warnings
              // |busyUntil| will listen for rejections.
              // Bug 1121100 may find a better way to silence these.
            });
-    return this.busyUntil(promise, "Connecting to " + name);
+    promise = this.busyUntil(promise, "Connecting to " + name);
+    // Stop busy timeout for runtimes that take unknown or long amounts of time
+    // to connect.
+    if (runtime.prolongedConnection) {
+      this.cancelBusyTimeout();
+    }
+    return promise;
   },
 
   updateRuntimeButton: function() {
     let labelNode = document.querySelector("#runtime-panel-button > .panel-button-label");
     if (!AppManager.selectedRuntime) {
       labelNode.setAttribute("value", Strings.GetStringFromName("runtimeButton_label"));
     } else {
       let name = AppManager.selectedRuntime.name;
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -61,16 +61,20 @@ const Strings = Services.strings.createB
  * |id| field
  *   An identifier that is unique in the set of all runtimes with the same
  *   |type|.  WebIDE tries to save the last used runtime via type + id, and
  *   tries to locate it again in the next session, so this value should attempt
  *   to be stable across Firefox sessions.
  * |name| field
  *   A user-visible label to identify the runtime that will be displayed in a
  *   runtime list.
+ * |prolongedConnection| field
+ *   A boolean value which should be |true| if the connection process is
+ *   expected to take a unknown or large amount of time.  A UI may use this as a
+ *   hint to skip timeouts or other time-based code paths.
  * connect()
  *   Configure the passed |connection| object with any settings need to
  *   successfully connect to the runtime, and call the |connection|'s connect()
  *   method.
  *   @param  Connection connection
  *           A |Connection| object from the DevTools |ConnectionManager|.
  *   @return Promise
  *           Resolved once you've called the |connection|'s connect() method.
@@ -441,16 +445,18 @@ DeprecatedUSBRuntime.prototype = {
 exports._DeprecatedUSBRuntime = DeprecatedUSBRuntime;
 
 function WiFiRuntime(deviceName) {
   this.deviceName = deviceName;
 }
 
 WiFiRuntime.prototype = {
   type: RuntimeTypes.WIFI,
+  // Mark runtime as taking a long time to connect
+  prolongedConnection: true,
   connect: function(connection) {
     let service = discovery.getRemoteService("devtools", this.deviceName);
     if (!service) {
       return promise.reject(new Error("Can't find device: " + this.name));
     }
     connection.advertisement = service;
     connection.authenticator.sendOOB = this.sendOOB;
     // Disable the default connection timeout, since QR scanning can take an
--- a/browser/devtools/webide/test/head.js
+++ b/browser/devtools/webide/test/head.js
@@ -32,16 +32,17 @@ Services.prefs.setCharPref("devtools.web
 
 
 SimpleTest.registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.webide.enabled");
   Services.prefs.clearUserPref("devtools.webide.enableLocalRuntime");
   Services.prefs.clearUserPref("devtools.webide.autoinstallADBHelper");
   Services.prefs.clearUserPref("devtools.webide.autoinstallFxdtAdapters");
   Services.prefs.clearUserPref("devtools.webide.sidebars");
+  Services.prefs.clearUserPref("devtools.webide.busyTimeout");
 });
 
 function openWebIDE(autoInstallAddons) {
   info("opening WebIDE");
 
   Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", !!autoInstallAddons);
   Services.prefs.setBoolPref("devtools.webide.autoinstallFxdtAdapters", !!autoInstallAddons);
 
--- a/browser/devtools/webide/test/test_runtime.html
+++ b/browser/devtools/webide/test/test_runtime.html
@@ -57,26 +57,50 @@
               return promise.resolve();
             },
 
             get name() {
               return "fakeRuntime";
             }
           });
 
+          win.AppManager.runtimeList.usb.push({
+            connect: function(connection) {
+              let deferred = promise.defer();
+              return deferred.promise;
+            },
+
+            get name() {
+              return "infiniteRuntime";
+            }
+          });
+
+          win.AppManager.runtimeList.usb.push({
+            connect: function(connection) {
+              let deferred = promise.defer();
+              return deferred.promise;
+            },
+
+            prolongedConnection: true,
+
+            get name() {
+              return "prolongedRuntime";
+            }
+          });
+
           win.AppManager.update("runtimelist");
 
           let packagedAppLocation = getTestFilePath("app");
 
           yield win.Cmds.importPackagedApp(packagedAppLocation);
           yield waitForUpdate(win, "project-validated");
 
           let panelNode = win.document.querySelector("#runtime-panel");
           let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
-          is(items.length, 1, "Found one runtime button");
+          is(items.length, 3, "Found 3 runtime buttons");
 
           let deferred = promise.defer();
           win.AppManager.connection.once(
               win.Connection.Events.CONNECTED,
               () => deferred.resolve());
 
           items[0].click();
 
@@ -99,17 +123,16 @@
           win.AppManager._selectedProject = oldProject;
           win.UI.updateCommands();
 
           yield nextTick();
 
           ok(isPlayActive(), "play button is enabled 3");
           ok(!isStopActive(), "stop button is disabled 3");
 
-
           yield win.Cmds.disconnectRuntime();
 
           is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
 
           ok(win.AppManager.selectedProject, "A project is still selected");
           ok(!isPlayActive(), "play button is disabled 4");
           ok(!isStopActive(), "stop button is disabled 4");
 
@@ -132,16 +155,49 @@
           // Toolbox opens automatically for main process / runtime apps
           ok(win.UI.toolboxPromise, "Toolbox promise exists");
           yield win.UI.toolboxPromise;
 
           ok(win.UI.toolboxIframe, "Toolbox iframe exists");
 
           yield win.Cmds.disconnectRuntime();
 
+          Services.prefs.setIntPref("devtools.webide.busyTimeout", 100);
+
+          // Wait for error message since connection never completes
+          let errorDeferred = promise.defer();
+          win.UI.reportError = errorName => {
+            if (errorName === "error_operationTimeout") {
+              errorDeferred.resolve();
+            }
+          };
+
+          // Click the infinite runtime
+          items[1].click();
+          ok(win.document.querySelector("window").className, "busy", "UI is busy");
+          yield errorDeferred.promise;
+
+          // Check for unexpected error message since this is prolonged
+          let noErrorDeferred = promise.defer();
+          win.UI.reportError = errorName => {
+            if (errorName === "error_operationTimeout") {
+              noErrorDeferred.reject();
+            }
+          };
+
+          // Click the prolonged runtime
+          items[2].click();
+          ok(win.document.querySelector("window").className, "busy", "UI is busy");
+
+          setTimeout(() => {
+            noErrorDeferred.resolve();
+          }, 1000);
+
+          yield noErrorDeferred.promise;
+
           SimpleTest.finish();
 
         });
       }
 
 
     </script>
   </body>
--- a/browser/devtools/webide/webide-prefs.js
+++ b/browser/devtools/webide/webide-prefs.js
@@ -8,17 +8,17 @@ pref("devtools.webide.templatesURL", "ht
 pref("devtools.webide.autoinstallADBHelper", true);
 pref("devtools.webide.autoinstallFxdtAdapters", true);
 pref("devtools.webide.autoConnectRuntime", true);
 pref("devtools.webide.restoreLastProject", true);
 pref("devtools.webide.enableLocalRuntime", false);
 pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json");
 pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi");
 pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org");
-pref("devtools.webide.simulatorAddonRegExp", "fxos_(.*)_simulator@mozilla\.org$");
+pref("devtools.webide.simulatorAddonRegExp", "fxos_(.*)_simulator@mozilla\\.org$");
 pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
 pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
 pref("devtools.webide.adaptersAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxdt-adapters/#OS#/fxdt-adapters-#OS#-latest.xpi");
 pref("devtools.webide.adaptersAddonID", "fxdevtools-adapters@mozilla.org");
 pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
 pref("devtools.webide.lastConnectedRuntime", "");
 pref("devtools.webide.lastSelectedProject", "");
 pref("devtools.webide.logSimulatorOutput", false);
--- a/browser/themes/shared/devtools/debugger.inc.css
+++ b/browser/themes/shared/devtools/debugger.inc.css
@@ -40,16 +40,26 @@
 }
 
 .dbg-breakpoint-checkbox {
   width: 16px;
   height: 16px;
   margin: 2px;
 }
 
+.dbg-breakpoint-condition-thrown-message {
+  display: none;
+  color: var(--theme-highlight-red);
+}
+
+.dbg-breakpoint.dbg-breakpoint-condition-thrown .dbg-breakpoint-condition-thrown-message {
+  display: block;
+  -moz-padding-start: 0;
+}
+
 /* Sources toolbar */
 
 #sources-toolbar > .devtools-toolbarbutton,
 #sources-controls > .devtools-toolbarbutton {
   min-width: 32px;
 }
 
 #black-box {
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -146,16 +146,21 @@ treecol {
 
 /* Content pane */
 #playDRMContentLink {
   /* Line up with the buttons in the other grid bits: */
   margin-left: 4px !important;
   margin-right: 4px !important;
 }
 
+#defaultFontSizeLabel {
+  /* !important needed to override common !important rule */
+  -moz-margin-start: 4px !important;
+}
+
 /* Applications Pane Styles */
 
 #applicationsContent {
   padding: 15px 0;
 }
 
 #filter {
   -moz-margin-start: 0;
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -25,16 +25,17 @@ want to export this environment variable
 
 # TODO Bug 794506 Integrate with the in-tree virtualenv configuration.
 SEARCH_PATHS = [
     'python/mach',
     'python/mozboot',
     'python/mozbuild',
     'python/mozversioncontrol',
     'python/blessings',
+    'python/compare-locales',
     'python/configobj',
     'python/jsmin',
     'python/psutil',
     'python/which',
     'python/pystache',
     'python/pyyaml/lib',
     'build/pymake',
     'config',
@@ -72,16 +73,17 @@ SEARCH_PATHS = [
 # Individual files providing mach commands.
 MACH_MODULES = [
     'addon-sdk/mach_commands.py',
     'build/valgrind/mach_commands.py',
     'dom/bindings/mach_commands.py',
     'layout/tools/reftest/mach_commands.py',
     'python/mach_commands.py',
     'python/mach/mach/commands/commandinfo.py',
+    'python/compare-locales/mach_commands.py',
     'python/mozboot/mozboot/mach_commands.py',
     'python/mozbuild/mozbuild/mach_commands.py',
     'python/mozbuild/mozbuild/backend/mach_commands.py',
     'python/mozbuild/mozbuild/frontend/mach_commands.py',
     'services/common/tests/mach_commands.py',
     'testing/mach_commands.py',
     'testing/taskcluster/mach_commands.py',
     'testing/marionette/mach_commands.py',
--- a/caps/DomainPolicy.cpp
+++ b/caps/DomainPolicy.cpp
@@ -1,26 +1,59 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
 /* vim: set ts=4 et sw=4 tw=80: */
 /* 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 "DomainPolicy.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/unused.h"
+#include "nsIMessageManager.h"
 #include "nsScriptSecurityManager.h"
 
 namespace mozilla {
 
+using namespace ipc;
+using namespace dom;
+
 NS_IMPL_ISUPPORTS(DomainPolicy, nsIDomainPolicy)
 
-DomainPolicy::DomainPolicy() : mBlacklist(new DomainSet())
-                             , mSuperBlacklist(new DomainSet())
-                             , mWhitelist(new DomainSet())
-                             , mSuperWhitelist(new DomainSet())
-{}
+static nsresult
+BroadcastDomainSetChange(DomainSetType aSetType, DomainSetChangeType aChangeType,
+                         nsIURI* aDomain = nullptr)
+{
+    MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default,
+               "DomainPolicy should only be exposed to the chrome process.");
+
+    nsTArray<ContentParent*> parents;
+    ContentParent::GetAll(parents);
+    if (!parents.Length()) {
+       return NS_OK;
+    }
+
+    OptionalURIParams uri;
+    SerializeURI(aDomain, uri);
+
+    for (uint32_t i = 0; i < parents.Length(); i++) {
+        unused << parents[i]->SendDomainSetChanged(aSetType, aChangeType, uri);
+    }
+    return NS_OK;
+}
+
+DomainPolicy::DomainPolicy() : mBlacklist(new DomainSet(BLACKLIST))
+                             , mSuperBlacklist(new DomainSet(SUPER_BLACKLIST))
+                             , mWhitelist(new DomainSet(WHITELIST))
+                             , mSuperWhitelist(new DomainSet(SUPER_WHITELIST))
+{
+    if (XRE_GetProcessType() == GeckoProcessType_Default) {
+        BroadcastDomainSetChange(NO_TYPE, ACTIVATE_POLICY);
+    }
+}
 
 DomainPolicy::~DomainPolicy()
 {
     // The SSM holds a strong ref to the DomainPolicy until Deactivate() is
     // invoked, so we should never hit the destructor until that happens.
     MOZ_ASSERT(!mBlacklist && !mSuperBlacklist &&
                !mWhitelist && !mSuperWhitelist);
 }
@@ -70,20 +103,57 @@ DomainPolicy::Deactivate()
 
     // Null them out.
     mBlacklist = nullptr;
     mSuperBlacklist = nullptr;
     mWhitelist = nullptr;
     mSuperWhitelist = nullptr;
 
     // Inform the SSM.
-    nsScriptSecurityManager::GetScriptSecurityManager()->DeactivateDomainPolicy();
+    nsScriptSecurityManager* ssm = nsScriptSecurityManager::GetScriptSecurityManager();
+    if (ssm) {
+        ssm->DeactivateDomainPolicy();
+    }
+    if (XRE_GetProcessType() == GeckoProcessType_Default) {
+        BroadcastDomainSetChange(NO_TYPE, DEACTIVATE_POLICY);
+    }
     return NS_OK;
 }
 
+void
+DomainPolicy::CloneDomainPolicy(DomainPolicyClone* aClone)
+{
+    aClone->active() = true;
+    static_cast<DomainSet*>(mBlacklist.get())->CloneSet(&aClone->blacklist());
+    static_cast<DomainSet*>(mSuperBlacklist.get())->CloneSet(&aClone->superBlacklist());
+    static_cast<DomainSet*>(mWhitelist.get())->CloneSet(&aClone->whitelist());
+    static_cast<DomainSet*>(mSuperWhitelist.get())->CloneSet(&aClone->superWhitelist());
+}
+
+static
+void
+CopyURIs(const InfallibleTArray<URIParams>& aDomains, nsIDomainSet* aSet)
+{
+    for (uint32_t i = 0; i < aDomains.Length(); i++) {
+        nsCOMPtr<nsIURI> uri = DeserializeURI(aDomains[i]);
+        aSet->Add(uri);
+    }
+}
+
+void
+DomainPolicy::ApplyClone(DomainPolicyClone* aClone)
+{
+    nsCOMPtr<nsIDomainSet> list;
+
+    CopyURIs(aClone->blacklist(), mBlacklist);
+    CopyURIs(aClone->whitelist(), mWhitelist);
+    CopyURIs(aClone->superBlacklist(), mSuperBlacklist);
+    CopyURIs(aClone->superWhitelist(), mSuperWhitelist);
+}
+
 static already_AddRefed<nsIURI>
 GetCanonicalClone(nsIURI* aURI)
 {
     nsCOMPtr<nsIURI> clone;
     nsresult rv = aURI->Clone(getter_AddRefs(clone));
     NS_ENSURE_SUCCESS(rv, nullptr);
     rv = clone->SetUserPass(EmptyCString());
     NS_ENSURE_SUCCESS(rv, nullptr);
@@ -95,32 +165,41 @@ GetCanonicalClone(nsIURI* aURI)
 NS_IMPL_ISUPPORTS(DomainSet, nsIDomainSet)
 
 NS_IMETHODIMP
 DomainSet::Add(nsIURI* aDomain)
 {
     nsCOMPtr<nsIURI> clone = GetCanonicalClone(aDomain);
     NS_ENSURE_TRUE(clone, NS_ERROR_FAILURE);
     mHashTable.PutEntry(clone);
+    if (XRE_GetProcessType() == GeckoProcessType_Default)
+        return BroadcastDomainSetChange(mType, ADD_DOMAIN, aDomain);
+
     return NS_OK;
 }
 
 NS_IMETHODIMP
 DomainSet::Remove(nsIURI* aDomain)
 {
     nsCOMPtr<nsIURI> clone = GetCanonicalClone(aDomain);
     NS_ENSURE_TRUE(clone, NS_ERROR_FAILURE);
     mHashTable.RemoveEntry(clone);
+    if (XRE_GetProcessType() == GeckoProcessType_Default)
+        return BroadcastDomainSetChange(mType, REMOVE_DOMAIN, aDomain);
+
     return NS_OK;
 }
 
 NS_IMETHODIMP
 DomainSet::Clear()
 {
     mHashTable.Clear();
+    if (XRE_GetProcessType() == GeckoProcessType_Default)
+        return BroadcastDomainSetChange(mType, CLEAR_DOMAINS);
+
     return NS_OK;
 }
 
 NS_IMETHODIMP
 DomainSet::Contains(nsIURI* aDomain, bool* aContains)
 {
     *aContains = false;
     nsCOMPtr<nsIURI> clone = GetCanonicalClone(aDomain);
@@ -155,9 +234,36 @@ DomainSet::ContainsSuperDomain(nsIURI* a
         NS_ENSURE_SUCCESS(rv, rv);
     }
 
     // No match.
     return NS_OK;
 
 }
 
+NS_IMETHODIMP
+DomainSet::GetType(uint32_t* aType)
+{
+    *aType = mType;
+    return NS_OK;
+}
+
+static
+PLDHashOperator
+DomainEnumerator(nsURIHashKey* aEntry, void* aUserArg)
+{
+    InfallibleTArray<URIParams>* uris = static_cast<InfallibleTArray<URIParams>*>(aUserArg);
+    nsIURI* key = aEntry->GetKey();
+
+    URIParams uri;
+    SerializeURI(key, uri);
+
+    uris->AppendElement(uri);
+    return PL_DHASH_NEXT;
+}
+
+void
+DomainSet::CloneSet(InfallibleTArray<URIParams>* aDomains)
+{
+    mHashTable.EnumerateEntries(DomainEnumerator, aDomains);
+}
+
 } /* namespace mozilla */
--- a/caps/DomainPolicy.h
+++ b/caps/DomainPolicy.h
@@ -8,16 +8,40 @@
 #define DomainPolicy_h__
 
 #include "nsIDomainPolicy.h"
 #include "nsTHashtable.h"
 #include "nsURIHashKey.h"
 
 namespace mozilla {
 
+namespace dom {
+class nsIContentParent;
+};
+
+namespace ipc {
+class URIParams;
+};
+
+enum DomainSetChangeType{
+    ACTIVATE_POLICY,
+    DEACTIVATE_POLICY,
+    ADD_DOMAIN,
+    REMOVE_DOMAIN,
+    CLEAR_DOMAINS
+};
+
+enum DomainSetType{
+    NO_TYPE,
+    BLACKLIST,
+    SUPER_BLACKLIST,
+    WHITELIST,
+    SUPER_WHITELIST
+};
+
 class DomainPolicy : public nsIDomainPolicy
 {
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIDOMAINPOLICY
     DomainPolicy();
 
 private:
@@ -30,18 +54,23 @@ private:
 };
 
 class DomainSet : public nsIDomainSet
 {
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIDOMAINSET
 
-    DomainSet() {}
+    explicit DomainSet(DomainSetType aType)
+        : mType(aType)
+    {}
+
+    void CloneSet(InfallibleTArray<mozilla::ipc::URIParams>* aDomains);
 
 protected:
     virtual ~DomainSet() {}
     nsTHashtable<nsURIHashKey> mHashTable;
+    DomainSetType mType;
 };
 
 } /* namespace mozilla */
 
 #endif /* DomainPolicy_h__ */
--- a/caps/nsIDomainPolicy.idl
+++ b/caps/nsIDomainPolicy.idl
@@ -3,43 +3,61 @@
  * 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"
 
 interface nsIURI;
 interface nsIDomainSet;
 
+%{ C++
+namespace mozilla {
+namespace dom {
+class DomainPolicyClone;
+}
+}
+%}
+
+[ptr] native DomainPolicyClonePtr(mozilla::dom::DomainPolicyClone);
+
 /*
  * When a domain policy is instantiated by invoking activateDomainPolicy() on
  * nsIScriptSecurityManager, these domain sets are consulted when each new
  * global is created (they have no effect on already-created globals).
  * If javascript is globally enabled with |javascript.enabled|, the blacklists
  * are consulted. If globally disabled, the whitelists are consulted. Lookups
  * on blacklist and whitelist happen with contains(), and lookups on
  * superBlacklist and superWhitelist happen with containsSuperDomain().
  *
  * When deactivate() is invoked, the domain sets are emptied, and the
  * nsIDomainPolicy ceases to have any effect on the system.
  */
-[scriptable, builtinclass, uuid(27b10f54-f34b-42b7-8594-4348d3ad7953)]
+[scriptable, builtinclass, uuid(82b24a20-6701-4d40-a0f9-f5dc7321b555)]
 interface nsIDomainPolicy : nsISupports
 {
     readonly attribute nsIDomainSet blacklist;
     readonly attribute nsIDomainSet superBlacklist;
     readonly attribute nsIDomainSet whitelist;
     readonly attribute nsIDomainSet superWhitelist;
 
     void deactivate();
+
+    [noscript, notxpcom] void cloneDomainPolicy(in DomainPolicyClonePtr aClone);
+    [noscript, notxpcom] void applyClone(in DomainPolicyClonePtr aClone);
 };
 
-[scriptable, builtinclass, uuid(946a01ff-6525-4007-a2c2-447ebe1875d3)]
+[scriptable, builtinclass, uuid(665c981b-0a0f-4229-ac06-a826e02d4f69)]
 interface nsIDomainSet : nsISupports
 {
     /*
+     * The type of the set. See: DomainSetType
+     */
+    [noscript] readonly attribute uint32_t type;
+
+    /*
      * Add a domain to the set. No-op if it already exists.
      */
     void add(in nsIURI aDomain);
 
     /*
      * Remove a domain from the set. No-op if it doesn't exist.
      */
     void remove(in nsIURI aDomain);
--- a/caps/nsIScriptSecurityManager.idl
+++ b/caps/nsIScriptSecurityManager.idl
@@ -9,22 +9,29 @@ interface nsIURI;
 interface nsIChannel;
 interface nsIClassInfo;
 interface nsIDocShell;
 interface nsIDomainPolicy;
 interface nsILoadContext;
 
 %{ C++
 #include "jspubtd.h"
+
+namespace mozilla {
+namespace dom {
+class DomainPolicyClone;
+}
+}
 %}
 
 [ptr] native JSContextPtr(JSContext);
 [ptr] native JSObjectPtr(JSObject);
+[ptr] native DomainPolicyClonePtr(mozilla::dom::DomainPolicyClone);
 
-[scriptable, uuid(f649959d-dae3-4027-83fd-5b7f8c8a8815)]
+[scriptable, uuid(ba602ca6-dc7a-457e-a57a-ee5b343fd863)]
 interface nsIScriptSecurityManager : nsISupports
 {
     /**
      * For each of these hooks returning NS_OK means 'let the action continue'.
      * Returning an error code means 'veto the action'. XPConnect will return
      * false to the js engine if the action is vetoed. The implementor of this
      * interface is responsible for setting a JS exception into the JSContext
      * if that is appropriate.
@@ -236,16 +243,32 @@ interface nsIScriptSecurityManager : nsI
      * nsIDomainPolicy returned from activateDomainPolicy(). At this point,
      * domainPolicyActive becomes false again, and a new consumer may acquire
      * control of the system by invoking activateDomainPolicy().
      */
     nsIDomainPolicy activateDomainPolicy();
     readonly attribute boolean domainPolicyActive;
 
     /**
+     * Only the parent process can directly access domain policies, child
+     * processes only have a read-only mirror to the one in the parent.
+     * For child processes the mirror is updated via messages
+     * and ContentChild will hold the DomainPolicy by calling
+     * ActivateDomainPolicyInternal directly. New consumer to this
+     * function should not be addded.
+     */
+    [noscript] nsIDomainPolicy activateDomainPolicyInternal();
+
+    /**
+     * This function is for internal use only. Every time a child process is spawned, we
+     * must clone any active domain policies in the parent to the new child.
+     */
+    [noscript, notxpcom] void cloneDomainPolicy(in DomainPolicyClonePtr aClone);
+
+    /**
      * Query mechanism for the above policy.
      *
      * If domainPolicyEnabled is false, this simply returns the current value
      * of javascript.enabled. Otherwise, it returns the same value, but taking
      * the various blacklist/whitelist exceptions into account.
      */
     bool policyAllowsScript(in nsIURI aDomain);
 };
--- a/caps/nsScriptSecurityManager.cpp
+++ b/caps/nsScriptSecurityManager.cpp
@@ -1305,19 +1305,24 @@ nsresult nsScriptSecurityManager::Init()
     return NS_OK;
 }
 
 static StaticRefPtr<nsScriptSecurityManager> gScriptSecMan;
 
 nsScriptSecurityManager::~nsScriptSecurityManager(void)
 {
     Preferences::RemoveObservers(this, kObservedPrefs);
-    if (mDomainPolicy)
+    if (mDomainPolicy) {
         mDomainPolicy->Deactivate();
-    MOZ_ASSERT(!mDomainPolicy);
+    }
+    // ContentChild might hold a reference to the domain policy,
+    // and it might release it only after the security manager is
+    // gone. But we can still assert this for the main process.
+    MOZ_ASSERT_IF(XRE_GetProcessType() == GeckoProcessType_Default,
+                  !mDomainPolicy);
 }
 
 void
 nsScriptSecurityManager::Shutdown()
 {
     if (sRuntime) {
         JS_SetSecurityCallbacks(sRuntime, nullptr);
         JS_SetTrustedPrincipals(sRuntime, nullptr);
@@ -1523,16 +1528,26 @@ nsScriptSecurityManager::GetDomainPolicy
 {
     *aRv = !!mDomainPolicy;
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsScriptSecurityManager::ActivateDomainPolicy(nsIDomainPolicy** aRv)
 {
+    if (XRE_GetProcessType() != GeckoProcessType_Default) {
+        return NS_ERROR_SERVICE_NOT_AVAILABLE;
+    }
+
+    return ActivateDomainPolicyInternal(aRv);
+}
+
+NS_IMETHODIMP
+nsScriptSecurityManager::ActivateDomainPolicyInternal(nsIDomainPolicy** aRv)
+{
     // We only allow one domain policy at a time. The holder of the previous
     // policy must explicitly deactivate it first.
     if (mDomainPolicy) {
         return NS_ERROR_SERVICE_NOT_AVAILABLE;
     }
 
     mDomainPolicy = new DomainPolicy();
     nsCOMPtr<nsIDomainPolicy> ptr = mDomainPolicy;
@@ -1543,16 +1558,27 @@ nsScriptSecurityManager::ActivateDomainP
 // Intentionally non-scriptable. Script must have a reference to the
 // nsIDomainPolicy to deactivate it.
 void
 nsScriptSecurityManager::DeactivateDomainPolicy()
 {
     mDomainPolicy = nullptr;
 }
 
+void
+nsScriptSecurityManager::CloneDomainPolicy(DomainPolicyClone* aClone)
+{
+    MOZ_ASSERT(aClone);
+    if (mDomainPolicy) {
+        mDomainPolicy->CloneDomainPolicy(aClone);
+    } else {
+        aClone->active() = false;
+    }
+}
+
 NS_IMETHODIMP
 nsScriptSecurityManager::PolicyAllowsScript(nsIURI* aURI, bool *aRv)
 {
     nsresult rv;
 
     // Compute our rule. If we don't have any domain policy set up that might
     // provide exceptions to this rule, we're done.
     *aRv = mIsJavaScriptEnabled;
--- a/dom/base/WebSocket.cpp
+++ b/dom/base/WebSocket.cpp
@@ -120,17 +120,18 @@ public:
 
   nsresult ParseURL(const nsAString& aURL);
   nsresult InitializeConnection();
 
   // These methods when called can release the WebSocket object
   void FailConnection(uint16_t reasonCode,
                       const nsACString& aReasonString = EmptyCString());
   nsresult CloseConnection(uint16_t reasonCode,
-                           const nsACString& aReasonString = EmptyCString());
+                           const nsACString& aReasonString = EmptyCString(),
+                           bool aCanceling = false);
   nsresult Disconnect();
   void DisconnectInternal();
 
   nsresult ConsoleError();
   nsresult PrintErrorOnConsole(const char* aBundleURI,
                                const char16_t* aError,
                                const char16_t** aFormatStrings,
                                uint32_t aFormatStringsLen);
@@ -411,16 +412,38 @@ private:
   // A raw pointer because this runnable is sync.
   WebSocketImpl* mImpl;
 
   uint16_t mReasonCode;
   const nsACString& mReasonString;
   nsresult mRv;
 };
 
+class CancelWebSocketRunnable final : public nsRunnable
+{
+public:
+  CancelWebSocketRunnable(nsIWebSocketChannel* aChannel, uint16_t aReasonCode,
+                          const nsACString& aReasonString)
+    : mChannel(aChannel)
+    , mReasonCode(aReasonCode)
+    , mReasonString(aReasonString)
+  {}
+
+  NS_IMETHOD Run() override
+  {
+    mChannel->Close(mReasonCode, mReasonString);
+    return NS_OK;
+  }
+
+private:
+  nsCOMPtr<nsIWebSocketChannel> mChannel;
+  uint16_t mReasonCode;
+  nsCString mReasonString;
+};
+
 class MOZ_STACK_CLASS MaybeDisconnect
 {
 public:
   explicit MaybeDisconnect(WebSocketImpl* aImpl)
     : mImpl(aImpl)
   {
   }
 
@@ -441,19 +464,21 @@ public:
 private:
   WebSocketImpl* mImpl;
 };
 
 } // anonymous namespace
 
 nsresult
 WebSocketImpl::CloseConnection(uint16_t aReasonCode,
-                               const nsACString& aReasonString)
+                               const nsACString& aReasonString,
+                               bool aCanceling)
 {
   AssertIsOnTargetThread();
+  MOZ_ASSERT(!NS_IsMainThread() || !aCanceling);
 
   if (mDisconnectingOrDisconnected) {
     return NS_OK;
   }
 
   // If this method is called because the worker is going away, we will not
   // receive the OnStop() method and we have to disconnect the WebSocket and
   // release the WorkerFeature.
@@ -470,16 +495,22 @@ WebSocketImpl::CloseConnection(uint16_t 
     mWebSocket->SetReadyState(WebSocket::CLOSING);
 
     // The channel has to be closed on the main-thread.
 
     if (NS_IsMainThread()) {
       return mChannel->Close(aReasonCode, aReasonString);
     }
 
+    if (aCanceling) {
+      nsRefPtr<CancelWebSocketRunnable> runnable =
+        new CancelWebSocketRunnable(mChannel, aReasonCode, aReasonString);
+      return NS_DispatchToMainThread(runnable);
+    }
+
     nsRefPtr<CloseRunnable> runnable =
       new CloseRunnable(this, aReasonCode, aReasonString);
     runnable->Dispatch(mWorkerPrivate->GetJSContext());
     return runnable->ErrorCode();
   }
 
   // No channel, but not disconnected: canceled or failed early
   //
@@ -504,16 +535,24 @@ WebSocketImpl::CloseConnection(uint16_t 
   return NS_OK;
 }
 
 nsresult
 WebSocketImpl::ConsoleError()
 {
   AssertIsOnTargetThread();
 
+  {
+    MutexAutoLock lock(mMutex);
+    if (mWorkerShuttingDown) {
+      // Too late to report anything, bail out.
+      return NS_OK;
+    }
+  }
+
   NS_ConvertUTF8toUTF16 specUTF16(mURI);
   const char16_t* formatStrings[] = { specUTF16.get() };
 
   if (mWebSocket->ReadyState() < WebSocket::OPEN) {
     PrintErrorOnConsole("chrome://global/locale/appstrings.properties",
                         MOZ_UTF16("connectionFailure"),
                         formatStrings, ArrayLength(formatStrings));
   } else {
@@ -1989,17 +2028,18 @@ public:
     MOZ_ASSERT(aStatus > workers::Running);
 
     if (aStatus >= Canceling) {
       {
         MutexAutoLock lock(mWebSocketImpl->mMutex);
         mWebSocketImpl->mWorkerShuttingDown = true;
       }
 
-      mWebSocketImpl->CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY);
+      mWebSocketImpl->CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY,
+                                      EmptyCString(), true);
     }
 
     return true;
   }
 
   bool Suspend(JSContext* aCx) override
   {
     {
@@ -2575,24 +2615,24 @@ public:
     , mWebSocketImpl(aImpl)
     , mEvent(aEvent)
   {
   }
 
   bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate)
   {
     aWorkerPrivate->AssertIsOnWorkerThread();
+    aWorkerPrivate->ModifyBusyCountFromWorker(aCx, true);
 
     // No messages when disconnected.
     if (mWebSocketImpl->mDisconnectingOrDisconnected) {
       NS_WARNING("Dispatching a WebSocket event after the disconnection!");
       return true;
     }
 
-    aWorkerPrivate->ModifyBusyCountFromWorker(aCx, true);
     return !NS_FAILED(mEvent->Run());
   }
 
   void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult)
   {
     aWorkerPrivate->ModifyBusyCountFromWorker(aCx, false);
   }
 
@@ -2617,32 +2657,32 @@ private:
 NS_IMETHODIMP
 WebSocketImpl::Dispatch(nsIRunnable* aEvent, uint32_t aFlags)
 {
   // If the target is the main-thread we can just dispatch the runnable.
   if (mIsMainThread) {
     return NS_DispatchToMainThread(aEvent);
   }
 
-  // If the target is a worker, we have to use a custom WorkerRunnableDispatcher
-  // runnable.
-  nsRefPtr<WorkerRunnableDispatcher> event =
-    new WorkerRunnableDispatcher(this, mWorkerPrivate, aEvent);
-
   MutexAutoLock lock(mMutex);
   if (mWorkerShuttingDown) {
     return NS_OK;
   }
 
   MOZ_ASSERT(mWorkerPrivate);
 
 #ifdef DEBUG
   MOZ_ASSERT(HasFeatureRegistered());
 #endif
 
+  // If the target is a worker, we have to use a custom WorkerRunnableDispatcher
+  // runnable.
+  nsRefPtr<WorkerRunnableDispatcher> event =
+    new WorkerRunnableDispatcher(this, mWorkerPrivate, aEvent);
+
   if (!event->Dispatch(nullptr)) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/dom/cache/Cache.cpp
+++ b/dom/cache/Cache.cpp
@@ -88,16 +88,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(mozilla::dom::cache::Cache)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal, mRequestPromises)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(mozilla::dom::cache::Cache)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Cache)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 Cache::Cache(nsIGlobalObject* aGlobal, CacheChild* aActor)
   : mGlobal(aGlobal)
   , mActor(aActor)
 {
   MOZ_ASSERT(mGlobal);
   MOZ_ASSERT(mActor);
--- a/dom/cache/CacheStorage.cpp
+++ b/dom/cache/CacheStorage.cpp
@@ -50,16 +50,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(mozilla::dom::cache::CacheStorage)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal, mRequestPromises)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(mozilla::dom::cache::CacheStorage)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CacheStorage)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIIPCBackgroundChildCreateCallback)
   NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback)
 NS_INTERFACE_MAP_END
 
 // static
 already_AddRefed<CacheStorage>
 CacheStorage::CreateOnMainThread(Namespace aNamespace, nsIGlobalObject* aGlobal,
                                  nsIPrincipal* aPrincipal, ErrorResult& aRv)
 {
--- a/dom/cache/test/mochitest/mochitest.ini
+++ b/dom/cache/test/mochitest/mochitest.ini
@@ -13,19 +13,23 @@ support-files =
   test_cache_overwrite.js
   mirror.sjs
   test_cache_match_vary.js
   vary.sjs
   test_caches.js
   test_cache_keys.js
   test_cache_put.js
   test_cache_requestCache.js
+  test_cache_delete.js
+  test_cache_put_reorder.js
 
 [test_cache.html]
 [test_cache_add.html]
 [test_cache_match_request.html]
 [test_cache_matchAll_request.html]
 [test_cache_overwrite.html]
 [test_cache_match_vary.html]
 [test_caches.html]
 [test_cache_keys.html]
 [test_cache_put.html]
 [test_cache_requestCache.html]
+[test_cache_delete.html]
+[test_cache_put_reorder.html]
new file mode 100644
--- /dev/null
+++ b/dom/cache/test/mochitest/test_cache_delete.html
@@ -0,0 +1,20 @@
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Validate the Cache.delete() method</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="driver.js"></script>
+</head>
+<body>
+<iframe id="frame"></iframe>
+<script class="testbody" type="text/javascript">
+  runTests("test_cache_delete.js")
+    .then(function() {
+      SimpleTest.finish();
+    });
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/cache/test/mochitest/test_cache_delete.js
@@ -0,0 +1,111 @@
+var name = "delete" + context;
+var c;
+
+function setupTest(reqs) {
+  return new Promise(function(resolve, reject) {
+    var cache;
+    caches.open(name).then(function(c) {
+        cache = c;
+        return c.addAll(reqs);
+      }).then(function() {
+        resolve(cache);
+      }).catch(function(err) {
+        reject(err);
+      });
+  });
+}
+
+function testBasics() {
+  var tests = [
+    "//mochi.test:8888/?foo" + context,
+    "//mochi.test:8888/?bar" + context,
+  ];
+  var cache;
+  return setupTest(tests)
+    .then(function(c) {
+      cache = c;
+      return cache.delete("//mochi.test:8888/?baz");
+    }).then(function(deleted) {
+      ok(!deleted, "Deleting a non-existing entry should fail");
+      return cache.keys();
+    }).then(function(keys) {
+      is(keys.length, 2, "No entries from the cache should be deleted");
+      return cache.delete(tests[0]);
+    }).then(function(deleted) {
+      ok(deleted, "Deleting an existing entry should succeed");
+      return cache.keys();
+    }).then(function(keys) {
+      is(keys.length, 1, "Only one entry should exist now");
+      ok(keys[0].url.indexOf(tests[1]) >= 0, "The correct entry must be deleted");
+    });
+}
+
+function testFragment() {
+  var tests = [
+    "//mochi.test:8888/?foo" + context,
+    "//mochi.test:8888/?bar" + context,
+    "//mochi.test:8888/?baz" + context + "#fragment",
+  ];
+  var cache;
+  return setupTest(tests)
+    .then(function(c) {
+      cache = c;
+      return cache.delete(tests[0] + "#fragment");
+    }).then(function(deleted) {
+      ok(deleted, "Deleting an existing entry should succeed");
+      return cache.keys();
+    }).then(function(keys) {
+      is(keys.length, 2, "Only one entry should exist now");
+      ok(keys[0].url.indexOf(tests[1]) >= 0, "The correct entry must be deleted");
+      ok(keys[1].url.indexOf(tests[2].replace("#fragment", "")) >= 0, "The correct entry must be deleted");
+      // Now, delete a request that was added with a fragment
+      return cache.delete("//mochi.test:8888/?baz" + context);
+    }).then(function(deleted) {
+      ok(deleted, "Deleting an existing entry should succeed");
+      return cache.keys();
+    }).then(function(keys) {
+      is(keys.length, 1, "Only one entry should exist now");
+      ok(keys[0].url.indexOf(tests[1]) >= 0, "3The correct entry must be deleted");
+    });
+}
+
+function testInterleaved() {
+  var tests = [
+    "//mochi.test:8888/?foo" + context,
+    "//mochi.test:8888/?bar" + context,
+  ];
+  var newURL = "//mochi.test:8888/?baz" + context;
+  var cache;
+  return setupTest(tests)
+    .then(function(c) {
+      cache = c;
+      // Simultaneously add and delete a request
+      return Promise.all([
+        cache.delete(newURL),
+        cache.add(newURL),
+      ]);
+    }).then(function(result) {
+      ok(!result[1], "deletion should fail");
+      return cache.keys();
+    }).then(function(keys) {
+      is(keys.length, 3, "Tree entries should still exist");
+      ok(keys[0].url.indexOf(tests[0]) >= 0, "The correct entry must be deleted");
+      ok(keys[1].url.indexOf(tests[1]) >= 0, "The correct entry must be deleted");
+      ok(keys[2].url.indexOf(newURL) >= 0, "The new entry should be correctly inserted");
+    });
+}
+
+// Make sure to clean up after each test step.
+function step(testPromise) {
+  return testPromise.then(function() {
+    caches.delete(name);
+  });
+}
+
+step(testBasics()).then(function() {
+  return step(testFragment());
+}).then(function() {
+  return step(testInterleaved());
+}).then(function() {
+  testDone();
+});
new file mode 100644
--- /dev/null
+++ b/dom/cache/test/mochitest/test_cache_put_reorder.html
@@ -0,0 +1,20 @@
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Ensure that using Cache.put to overwrite an entry will change its insertion order</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="driver.js"></script>
+</head>
+<body>
+<iframe id="frame"></iframe>
+<script class="testbody" type="text/javascript">
+  runTests("test_cache_put_reorder.js")
+    .then(function() {
+      SimpleTest.finish();
+    });
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/cache/test/mochitest/test_cache_put_reorder.js
@@ -0,0 +1,31 @@
+var name = "putreorder" + context;
+var c;
+
+var reqs = [
+  "//mochi.test:8888/?foo" + context,
+  "//mochi.test:8888/?bar" + context,
+  "//mochi.test:8888/?baz" + context,
+];
+
+caches.open(name).then(function(cache) {
+  c = cache;
+  return c.addAll(reqs);
+}).then(function() {
+  return c.put(reqs[1], new Response("overwritten"));
+}).then(function() {
+  return c.keys();
+}).then(function(keys) {
+  is(keys.length, 3, "Correct number of entries expected");
+  ok(keys[0].url.indexOf(reqs[0]) >= 0, "The first entry should be untouched");
+  ok(keys[2].url.indexOf(reqs[1]) >= 0, "The second entry should be moved to the end");
+  ok(keys[1].url.indexOf(reqs[2]) >= 0, "The third entry should now be the second one");
+  return c.match(reqs[1]);
+}).then(function(r) {
+  return r.text();
+}).then(function(body) {
+  is(body, "overwritten", "The body should be overwritten");
+  return caches.delete(name);
+}).then(function(deleted) {
+  ok(deleted, "The cache should be deleted successfully");
+  testDone();
+});
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -12,16 +12,17 @@
 #include "nsAttrValueInlines.h"
 
 #include "nsIDOMHTMLInputElement.h"
 #include "nsITextControlElement.h"
 #include "nsIDOMNSEditableElement.h"
 #include "nsIRadioVisitor.h"
 #include "nsIPhonetic.h"
 
+#include "mozilla/Telemetry.h"
 #include "nsIControllers.h"
 #include "nsIStringBundle.h"
 #include "nsFocusManager.h"
 #include "nsColorControlFrame.h"
 #include "nsNumberControlFrame.h"
 #include "nsPIDOMWindow.h"
 #include "nsRepeatService.h"
 #include "nsContentCID.h"
@@ -4492,16 +4493,22 @@ HTMLInputElement::BindToTree(nsIDocument
   // If there is a disabled fieldset in the parent chain, the element is now
   // barred from constraint validation and can't suffer from value missing
   // (call done before).
   UpdateBarredFromConstraintValidation();
 
   // And now make sure our state is up to date
   UpdateState(false);
 
+#ifdef EARLY_BETA_OR_EARLIER
+  if (mType == NS_FORM_INPUT_PASSWORD) {
+    Telemetry::Accumulate(Telemetry::PWMGR_PASSWORD_INPUT_IN_FORM, !!mForm);
+  }
+#endif
+
   return rv;
 }
 
 void
 HTMLInputElement::UnbindFromTree(bool aDeep, bool aNullParent)
 {
   // If we have a form and are unbound from it,
   // nsGenericHTMLFormElementWithState::UnbindFromTree() will unset the form and
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -23,16 +23,17 @@
 #ifdef ACCESSIBILITY
 #include "mozilla/a11y/DocAccessibleChild.h"
 #endif
 #include "mozilla/Preferences.h"
 #include "mozilla/ProcessHangMonitorIPC.h"
 #include "mozilla/docshell/OfflineCacheUpdateChild.h"
 #include "mozilla/dom/ContentBridgeChild.h"
 #include "mozilla/dom/ContentBridgeParent.h"
+#include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/DOMStorageIPC.h"
 #include "mozilla/dom/ExternalHelperAppChild.h"
 #include "mozilla/dom/PCrashReporterChild.h"
 #include "mozilla/dom/ProcessGlobal.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/asmjscache/AsmJSCache.h"
 #include "mozilla/dom/asmjscache/PAsmJSCacheEntryChild.h"
 #include "mozilla/dom/nsIContentChild.h"
@@ -105,18 +106,19 @@
 
 #include "nsChromeRegistryContent.h"
 #include "nsFrameMessageManager.h"
 
 #include "nsIGeolocationProvider.h"
 #include "mozilla/dom/PMemoryReportRequestChild.h"
 #include "mozilla/dom/PCycleCollectWithLogsChild.h"
 
+#include "nsIScriptSecurityManager.h"
+
 #ifdef MOZ_PERMISSIONS
-#include "nsIScriptSecurityManager.h"
 #include "nsPermission.h"
 #include "nsPermissionManager.h"
 #endif
 
 #include "PermissionMessageUtils.h"
 
 #if defined(MOZ_WIDGET_ANDROID)
 #include "APKOpen.h"
@@ -163,16 +165,17 @@
 
 #include "ProcessUtils.h"
 #include "StructuredCloneUtils.h"
 #include "URIUtils.h"
 #include "nsContentUtils.h"
 #include "nsIPrincipal.h"
 #include "nsDeviceStorage.h"
 #include "AudioChannelService.h"
+#include "DomainPolicy.h"
 #include "mozilla/dom/DataStoreService.h"
 #include "mozilla/dom/telephony/PTelephonyChild.h"
 #include "mozilla/dom/time/DateCacheCleaner.h"
 #include "mozilla/dom/voicemail/VoicemailIPCService.h"
 #include "mozilla/net/NeckoMessageUtils.h"
 #include "mozilla/RemoteSpellCheckEngineChild.h"
 
 using namespace mozilla;
@@ -781,19 +784,31 @@ ContentChild::InitXPCOM()
     }
 
     mConsoleListener = new ConsoleListener(this);
     if (NS_FAILED(svc->RegisterListener(mConsoleListener)))
         NS_WARNING("Couldn't register console listener for child process");
 
     bool isOffline;
     ClipboardCapabilities clipboardCaps;
-    SendGetXPCOMProcessAttributes(&isOffline, &mAvailableDictionaries, &clipboardCaps);
+    DomainPolicyClone domainPolicy;
+
+    SendGetXPCOMProcessAttributes(&isOffline, &mAvailableDictionaries, &clipboardCaps, &domainPolicy);
     RecvSetOffline(isOffline);
 
+    if (domainPolicy.active()) {
+        nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+        MOZ_ASSERT(ssm);
+        ssm->ActivateDomainPolicyInternal(getter_AddRefs(mPolicy));
+        if (!mPolicy) {
+            MOZ_CRASH("Failed to activate domain policy.");
+        }
+        mPolicy->ApplyClone(&domainPolicy);
+    }
+
     nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1"));
     if (nsCOMPtr<nsIClipboardProxy> clipboardProxy = do_QueryInterface(clipboard)) {
         clipboardProxy->SetCapabilities(clipboardCaps);
     }
 
     DebugOnly<FileUpdateDispatcher*> observer = FileUpdateDispatcher::GetSingleton();
     NS_ASSERTION(observer, "FileUpdateDispatcher is null");
 
@@ -2591,18 +2606,92 @@ bool
 ContentChild::RecvAssociatePluginId(const uint32_t& aPluginId,
                                     const base::ProcessId& aProcessId)
 {
     plugins::PluginModuleContentParent::AssociatePluginId(aPluginId, aProcessId);
     return true;
 }
 
 bool
+ContentChild::RecvDomainSetChanged(const uint32_t& aSetType, const uint32_t& aChangeType,
+                                   const OptionalURIParams& aDomain)
+{
+    if (aChangeType == ACTIVATE_POLICY) {
+        if (mPolicy) {
+            return true;
+        }
+        nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+        MOZ_ASSERT(ssm);
+        ssm->ActivateDomainPolicyInternal(getter_AddRefs(mPolicy));
+        return !!mPolicy;
+    } else if (!mPolicy) {
+        MOZ_ASSERT_UNREACHABLE("If the domain policy is not active yet,"
+                               " the first message should be ACTIVATE_POLICY");
+        return false;
+    }
+
+    NS_ENSURE_TRUE(mPolicy, false);
+
+    if (aChangeType == DEACTIVATE_POLICY) {
+        mPolicy->Deactivate();
+        mPolicy = nullptr;
+        return true;
+    }
+
+    nsCOMPtr<nsIDomainSet> set;
+    switch(aSetType) {
+        case BLACKLIST:
+            mPolicy->GetBlacklist(getter_AddRefs(set));
+            break;
+        case SUPER_BLACKLIST:
+            mPolicy->GetSuperBlacklist(getter_AddRefs(set));
+            break;
+        case WHITELIST:
+            mPolicy->GetWhitelist(getter_AddRefs(set));
+            break;
+        case SUPER_WHITELIST:
+            mPolicy->GetSuperWhitelist(getter_AddRefs(set));
+            break;
+        default:
+            NS_NOTREACHED("Unexpected setType");
+            return false;
+    }
+
+    MOZ_ASSERT(set);
+
+    nsCOMPtr<nsIURI> uri = DeserializeURI(aDomain);
+
+    switch(aChangeType) {
+        case ADD_DOMAIN:
+            NS_ENSURE_TRUE(uri, false);
+            set->Add(uri);
+            break;
+        case REMOVE_DOMAIN:
+            NS_ENSURE_TRUE(uri, false);
+            set->Remove(uri);
+            break;
+        case CLEAR_DOMAINS:
+            set->Clear();
+            break;
+        default:
+            NS_NOTREACHED("Unexpected changeType");
+            return false;
+    }
+
+    return true;
+}
+
+bool
 ContentChild::RecvShutdown()
 {
+    if (mPolicy) {
+        mPolicy->Deactivate();
+        mPolicy = nullptr;
+    }
+
     nsCOMPtr<nsIObserverService> os = services::GetObserverService();
     if (os) {
         os->NotifyObservers(this, "content-child-shutdown", nullptr);
     }
 
     GetIPCChannel()->SetAbortOnError(false);
 
     // Ignore errors here. If this fails, the parent will kill us after a
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -19,16 +19,17 @@
 #include "nsWeakPtr.h"
 
 
 struct ChromePackage;
 class nsIDOMBlob;
 class nsIObserver;
 struct ResourceMapping;
 struct OverrideMapping;
+class nsIDomainPolicy;
 
 namespace mozilla {
 class RemoteSpellcheckEngineChild;
 
 namespace ipc {
 class OptionalURIParams;
 class PFileDescriptorSetChild;
 class URIParams;
@@ -379,16 +380,18 @@ public:
                                       const bool& aResult) override;
 
     virtual bool RecvStartProfiler(const uint32_t& aEntries,
                                    const double& aInterval,
                                    nsTArray<nsCString>&& aFeatures,
                                    nsTArray<nsCString>&& aThreadNameFilters) override;
     virtual bool RecvStopProfiler() override;
     virtual bool RecvGetProfile(nsCString* aProfile) override;
+    virtual bool RecvDomainSetChanged(const uint32_t& aSetType, const uint32_t& aChangeType,
+                                      const OptionalURIParams& aDomain) override;
     virtual bool RecvShutdown() override;
 
 #ifdef ANDROID
     gfxIntSize GetScreenSize() { return mScreenSize; }
 #endif
 
     // Get the directory for IndexedDB files. We query the parent for this and
     // cache the value
@@ -476,16 +479,18 @@ private:
     bool mIsForApp;
     bool mIsForBrowser;
     bool mCanOverrideProcessName;
     bool mIsAlive;
     nsString mProcessName;
 
     static ContentChild* sSingleton;
 
+    nsCOMPtr<nsIDomainPolicy> mPolicy;
+
     DISALLOW_EVIL_CONSTRUCTORS(ContentChild);
 };
 
 uint64_t
 NextWindowID();
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -2777,18 +2777,19 @@ ContentParent::RecvAddNewProcess(const u
     for (size_t i = 0; i < numNuwaPrefUpdates; i++) {
         mozilla::unused << content->SendPreferenceUpdate(sNuwaPrefUpdates->ElementAt(i));
     }
 
     // Update offline settings.
     bool isOffline;
     InfallibleTArray<nsString> unusedDictionaries;
     ClipboardCapabilities clipboardCaps;
+    DomainPolicyClone domainPolicy;
     RecvGetXPCOMProcessAttributes(&isOffline, &unusedDictionaries,
-                                  &clipboardCaps);
+                                  &clipboardCaps, &domainPolicy);
     mozilla::unused << content->SendSetOffline(isOffline);
     MOZ_ASSERT(!clipboardCaps.supportsSelectionClipboard() &&
                !clipboardCaps.supportsFindClipboard(),
                "Unexpected values");
 
     PreallocatedProcessManager::PublishSpareProcess(content);
     return true;
 #else
@@ -3114,17 +3115,18 @@ ContentParent::RecvGetProcessAttributes(
     *aIsForBrowser = mIsForBrowser;
 
     return true;
 }
 
 bool
 ContentParent::RecvGetXPCOMProcessAttributes(bool* aIsOffline,
                                              InfallibleTArray<nsString>* dictionaries,
-                                             ClipboardCapabilities* clipboardCaps)
+                                             ClipboardCapabilities* clipboardCaps,
+                                             DomainPolicyClone* domainPolicy)
 {
     nsCOMPtr<nsIIOService> io(do_GetIOService());
     MOZ_ASSERT(io, "No IO service?");
     DebugOnly<nsresult> rv = io->GetOffline(aIsOffline);
     MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed getting offline?");
 
     nsCOMPtr<nsISpellChecker> spellChecker(do_GetService(NS_SPELLCHECKER_CONTRACTID));
     MOZ_ASSERT(spellChecker, "No spell checker?");
@@ -3135,16 +3137,21 @@ ContentParent::RecvGetXPCOMProcessAttrib
     MOZ_ASSERT(clipboard, "No clipboard?");
 
     rv = clipboard->SupportsSelectionClipboard(&clipboardCaps->supportsSelectionClipboard());
     MOZ_ASSERT(NS_SUCCEEDED(rv));
 
     rv = clipboard->SupportsFindClipboard(&clipboardCaps->supportsFindClipboard());
     MOZ_ASSERT(NS_SUCCEEDED(rv));
 
+    // Let's copy the domain policy from the parent to the child (if it's active).
+    nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+    NS_ENSURE_TRUE(ssm, false);
+    ssm->CloneDomainPolicy(domainPolicy);
+
     return true;
 }
 
 mozilla::jsipc::PJavaScriptParent *
 ContentParent::AllocPJavaScriptParent()
 {
     MOZ_ASSERT(!ManagedPJavaScriptParent().Length());
     return nsIContentParent::AllocPJavaScriptParent();
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -506,17 +506,18 @@ private:
     AllocPProcessHangMonitorParent(Transport* aTransport,
                                    ProcessId aOtherProcess) override;
 
     virtual bool RecvGetProcessAttributes(ContentParentId* aCpId,
                                           bool* aIsForApp,
                                           bool* aIsForBrowser) override;
     virtual bool RecvGetXPCOMProcessAttributes(bool* aIsOffline,
                                                InfallibleTArray<nsString>* dictionaries,
-                                               ClipboardCapabilities* clipboardCaps)
+                                               ClipboardCapabilities* clipboardCaps,
+                                               DomainPolicyClone* domainPolicy)
         override;
 
     virtual bool DeallocPJavaScriptParent(mozilla::jsipc::PJavaScriptParent*) override;
 
     virtual bool DeallocPRemoteSpellcheckEngineParent(PRemoteSpellcheckEngineParent*) override;
     virtual PBrowserParent* AllocPBrowserParent(const TabId& aTabId,
                                                 const IPCTabContext& aContext,
                                                 const uint32_t& aChromeFlags,
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -341,16 +341,25 @@ union FileDescOrError {
 };
 
 union OptionalContentId
 {
   ContentParentId;
   void_t;
 };
 
+struct DomainPolicyClone
+{
+    bool        active;
+    URIParams[] blacklist;
+    URIParams[] whitelist;
+    URIParams[] superBlacklist;
+    URIParams[] superWhitelist;
+};
+
 prio(normal upto urgent) sync protocol PContent
 {
     parent spawns PPluginModule;
 
     parent opens PCompositor;
     parent opens PProcessHangMonitor;
     parent opens PSharedBufferManager;
     parent opens PImageBridge;
@@ -547,16 +556,18 @@ child:
     async StartProfiler(uint32_t aEntries, double aInterval, nsCString[] aFeatures,
                         nsCString[] aThreadNameFilters);
     async StopProfiler();
     prio(high) sync GetProfile()
       returns (nsCString aProfile);
 
     NuwaFreeze();
 
+    async DomainSetChanged(uint32_t aSetType, uint32_t aChangeType, OptionalURIParams aDomain);
+
     /**
      * Notify the child to shutdown. The child will in turn call FinishShutdown
      * and let the parent close the channel.
      */
     async Shutdown();
 
     async LoadProcessScript(nsString url);
 
@@ -581,17 +592,18 @@ parent:
      * !isForBrowser|, we're probably loading <xul:browser remote>.
      *
      * Keep the return values in sync with PBrowser()!
      */
     sync GetProcessAttributes()
         returns (ContentParentId cpId, bool isForApp, bool isForBrowser);
     sync GetXPCOMProcessAttributes()
         returns (bool isOffline, nsString[] dictionaries,
-                 ClipboardCapabilities clipboardCaps);
+                 ClipboardCapabilities clipboardCaps,
+                 DomainPolicyClone domainPolicy);
 
     sync CreateChildProcess(IPCTabContext context,
                             ProcessPriority priority,
                             TabId openerTabId)
         returns (ContentParentId cpId, bool isForApp, bool isForBrowser, TabId tabId);
     sync BridgeToChildProcess(ContentParentId cpId);
 
     /**
--- a/dom/ipc/moz.build
+++ b/dom/ipc/moz.build
@@ -149,12 +149,13 @@ if CONFIG['MOZ_TOOLKIT_SEARCH']:
     DEFINES['MOZ_TOOLKIT_SEARCH'] = True
 
 for var in ('MOZ_PERMISSIONS', 'MOZ_CHILD_PERMISSIONS'):
     if CONFIG[var]:
         DEFINES[var] = True
 
 JAR_MANIFESTS += ['jar.mn']
 
+BROWSER_CHROME_MANIFESTS += ['tests/browser.ini']
 MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
 MOCHITEST_MANIFESTS += ['tests/mochitest.ini']
 
 CXXFLAGS += CONFIG['TK_CFLAGS']
new file mode 100644
--- /dev/null
+++ b/dom/ipc/tests/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+  file_disableScript.html
+  file_domainPolicy_base.html
+
+[browser_domainPolicy.js]
new file mode 100644
--- /dev/null
+++ b/dom/ipc/tests/browser_domainPolicy.js
@@ -0,0 +1,240 @@
+var policy; // To make sure we never leave up an activated domain policy after a failed test, let's make this global.
+function activateDomainPolicy() {
+  const ssm = Services.scriptSecurityManager;
+  policy = ssm.activateDomainPolicy();
+}
+
+function deactivateDomainPolicy() {
+  if (policy) {
+    policy.deactivate();
+    policy = null;
+  }
+}
+
+function* test_domainPolicy() {
+
+  XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm");
+  let deferred = Promise.defer();
+  let currentTask = deferred.promise;
+  SpecialPowers.pushPrefEnv(
+    {set: [["dom.ipc.browser_frames.oop_by_default", false],
+          ["browser.pagethumbnails.capturing_disabled", false],
+          ["dom.mozBrowserFramesEnabled", false]]},
+    () => { return deferred.resolve()});
+  yield currentTask;
+
+  // Create tab
+  let tab;
+
+  // Init test
+  function initProcess() {
+    tab = gBrowser.addTab();
+    gBrowser.selectedTab = tab;
+
+    let initPromise = ContentTask.spawn(tab.linkedBrowser, null, function() {
+      Cu.import("resource://gre/modules/PromiseUtils.jsm");
+      function loadBase() {
+        let deferred = PromiseUtils.defer();
+        let listener = (event) => {
+          removeEventListener("DOMDocElementInserted", listener, true);
+          let listener2 = (event) => {
+            content.removeEventListener('load', listener2);
+            deferred.resolve();
+          }
+          content.addEventListener('load', listener2);
+        };
+        addEventListener("DOMDocElementInserted", listener, true);
+        return deferred.promise;
+      }
+
+      return loadBase();
+    });
+    tab.linkedBrowser.loadURI("http://mochi.test:8888/browser/dom/ipc/tests/file_domainPolicy_base.html");
+    return initPromise;
+  }
+
+  // We use ContentTask for the tests, but we also want to pass some data and some helper functions too.
+  // To do that, we serialize an input object via JSON |ipcArgs| and some shared helper functions |initUtils|
+  // and eval them in the content process.
+  var ipcArgs = {};
+  function initUtils(obj) {
+    obj.checkScriptEnabled = function(win, expectEnabled) {
+      win.wrappedJSObject.gFiredOnclick = false;
+      win.document.body.dispatchEvent(new win.Event('click'));
+      return { passed: win.wrappedJSObject.gFiredOnclick == expectEnabled,
+               msg: `Checking script-enabled for ${win.name} (${win.location})`};
+    }
+
+    obj.navigateFrame = function(ifr, src) {
+      let deferred = PromiseUtils.defer();
+      function onload() {
+        ifr.removeEventListener('load', onload);
+        deferred.resolve();
+      }
+      ifr.addEventListener('load', onload, false);
+      ifr.setAttribute('src', src);
+      return deferred.promise;
+    }
+  };
+
+  function runTest(test) {
+    return ContentTask.spawn(tab.linkedBrowser,
+      'ipcArgs = ' + JSON.stringify(ipcArgs) + '; (' + initUtils.toSource() + ')(utils)', test);
+  }
+
+  function checkAndCleanup(result) {
+    result = [].concat(result);
+    for (var i in result)
+      ok(result[i].passed, result[i].msg);
+    gBrowser.removeTab(tab);
+    deactivateDomainPolicy();
+    ipcArgs = {};
+  }
+
+  function testDomain(domain) {
+    ipcArgs.domain = domain;
+    return (aUtils) => {
+      Cu.import("resource://gre/modules/PromiseUtils.jsm");
+      var ipcArgs;
+      var utils = {};
+      eval(aUtils);
+
+      let path = '/browser/dom/ipc/tests/file_disableScript.html';
+      let deferred = PromiseUtils.defer();
+      var rootFrame = content.document.getElementById('root');
+      utils.navigateFrame(rootFrame, ipcArgs.domain + path).then(() => {
+        deferred.resolve(utils.checkScriptEnabled(rootFrame.contentWindow, false));
+      });
+      return deferred.promise;
+    }
+  }
+
+  info("Testing simple blacklist policy");
+
+  info("Creating child process first, activating domainPolicy after");
+  currentTask = initProcess();
+  yield currentTask;
+  activateDomainPolicy();
+  var bl = policy.blacklist;
+  bl.add(Services.io.newURI('http://example.com', null, null));
+  currentTask = runTest(testDomain("http://example.com"));
+  checkAndCleanup(yield currentTask);
+
+  info("Activating domainPolicy first, creating child process after");
+  activateDomainPolicy();
+  var bl = policy.blacklist;
+  bl.add(Services.io.newURI('http://example.com', null, null));
+  currentTask = initProcess();
+  yield currentTask;
+  currentTask = runTest(testDomain("http://example.com"));
+  checkAndCleanup(yield currentTask);
+
+  function testList(expectEnabled, list) {
+    ipcArgs.expectEnabled = expectEnabled;
+    ipcArgs.list = list;
+    return (aUtils) => {
+      Cu.import("resource://gre/modules/PromiseUtils.jsm");
+      var ipcArgs;
+      var utils = {};
+      eval(aUtils);
+
+      var results = [];
+      var testListInternal = function(expectEnabled, list, idx) {
+        idx = idx || 0;
+        let deferred = PromiseUtils.defer();
+        let path = '/browser/dom/ipc/tests/file_disableScript.html';
+        let target = list[idx] + path;
+        var rootFrame = content.document.getElementById('root');
+        utils.navigateFrame(rootFrame, target).then(function() {
+          results.push(utils.checkScriptEnabled(rootFrame.contentWindow, expectEnabled));
+          if (idx == list.length - 1)
+            deferred.resolve(results);
+          else
+            testListInternal(expectEnabled, list, idx + 1).then(function(retArg) { deferred.resolve(retArg); });
+        });
+        return deferred.promise;
+      }
+      return testListInternal(ipcArgs.expectEnabled, ipcArgs.list);
+    }
+  }
+
+  let testPolicy = {
+     exceptions: ['http://test1.example.com', 'http://example.com'],
+     superExceptions: ['http://test2.example.org', 'https://test1.example.com'],
+     exempt: ['http://test1.example.com', 'http://example.com',
+              'http://test2.example.org', 'http://sub1.test2.example.org',
+              'https://sub1.test1.example.com'],
+     notExempt: ['http://test2.example.com', 'http://sub1.test1.example.com',
+                 'http://www.example.com', 'https://test2.example.com',
+                 'https://example.com', 'http://test1.example.org'],
+  };
+
+  function activate(isBlack, exceptions, superExceptions) {
+    activateDomainPolicy();
+    let set = isBlack ? policy.blacklist : policy.whitelist;
+    let superSet = isBlack ? policy.superBlacklist : policy.superWhitelist;
+    for (var e of exceptions)
+      set.add(makeURI(e));
+    for (var e of superExceptions)
+      superSet.add(makeURI(e));
+  };
+
+  info("Testing Blacklist-style Domain Policy");
+  info("Activating domainPolicy first, creating child process after");
+  activate(true, testPolicy.exceptions, testPolicy.superExceptions);
+  currentTask = initProcess();
+  yield currentTask;
+  let results = [];
+  currentTask = runTest(testList(true, testPolicy.notExempt));
+  results = results.concat(yield currentTask);
+  currentTask = runTest(testList(false, testPolicy.exempt));
+  results = results.concat(yield currentTask);
+  checkAndCleanup(results);
+
+  info("Creating child process first, activating domainPolicy after");
+  currentTask = initProcess();
+  yield currentTask;
+  activate(true, testPolicy.exceptions, testPolicy.superExceptions);
+  results = [];
+  currentTask = runTest(testList(true, testPolicy.notExempt));
+  results = results.concat(yield currentTask);
+  currentTask = runTest(testList(false, testPolicy.exempt));
+  results = results.concat(yield currentTask);
+  checkAndCleanup(results);
+
+  info("Testing Whitelist-style Domain Policy");
+  deferred = Promise.defer();
+  currentTask = deferred.promise;
+  SpecialPowers.pushPrefEnv({set:[["javascript.enabled", false]]}, () => { return deferred.resolve()});
+  yield currentTask;
+
+  info("Activating domainPolicy first, creating child process after");
+  activate(false, testPolicy.exceptions, testPolicy.superExceptions);
+  currentTask = initProcess();
+  yield currentTask;
+  results = [];
+  currentTask = runTest(testList(false, testPolicy.notExempt));
+  results = results.concat(yield currentTask);
+  currentTask = runTest(testList(true, testPolicy.exempt));
+  results = results.concat(yield currentTask);
+  checkAndCleanup(results);
+
+  info("Creating child process first, activating domainPolicy after");
+  currentTask = initProcess();
+  yield currentTask;
+  activate(false, testPolicy.exceptions, testPolicy.superExceptions);
+  results = [];
+  currentTask = runTest(testList(false, testPolicy.notExempt));
+  results = results.concat(yield currentTask);
+  currentTask = runTest(testList(true, testPolicy.exempt));
+  results = results.concat(yield currentTask);
+  checkAndCleanup(results);
+  finish();
+}
+
+
+add_task(test_domainPolicy);
+
+registerCleanupFunction(()=>{
+  deactivateDomainPolicy();
+})
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/ipc/tests/file_disableScript.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+var gFiredOnload = false;
+var gFiredOnclick = false;
+</script>
+</head>
+<body onload="gFiredOnload = true;" onclick="gFiredOnclick = true;">
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/ipc/tests/file_domainPolicy_base.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<iframe id="root" name="root"/>
+</body>
+</html>
--- a/dom/security/nsMixedContentBlocker.cpp
+++ b/dom/security/nsMixedContentBlocker.cpp
@@ -281,26 +281,60 @@ nsMixedContentBlocker::AsyncOnChannelRed
   if (!NS_CP_ACCEPTED(decision)) {
     autoCallback.DontCallback();
     return NS_BINDING_FAILED;
   }
 
   return NS_OK;
 }
 
+/* This version of ShouldLoad() is non-static and called by the Content Policy
+ * API and AsyncOnChannelRedirect().  See nsIContentPolicy::ShouldLoad()
+ * for detailed description of the parameters.
+ */
 NS_IMETHODIMP
 nsMixedContentBlocker::ShouldLoad(uint32_t aContentType,
                                   nsIURI* aContentLocation,
                                   nsIURI* aRequestingLocation,
                                   nsISupports* aRequestingContext,
                                   const nsACString& aMimeGuess,
                                   nsISupports* aExtra,
                                   nsIPrincipal* aRequestPrincipal,
                                   int16_t* aDecision)
 {
+  // We pass in false as the first parameter to ShouldLoad(), because the
+  // callers of this method don't know whether the load went through cached
+  // image redirects.  This is handled by direct callers of the static
+  // ShouldLoad.
+  nsresult rv = ShouldLoad(false,   //aHadInsecureImageRedirect
+                           aContentType,
+                           aContentLocation,
+                           aRequestingLocation,
+                           aRequestingContext,
+                           aMimeGuess,
+                           aExtra,
+                           aRequestPrincipal,
+                           aDecision);
+  return rv;
+}
+
+/* Static version of ShouldLoad() that contains all the Mixed Content Blocker
+ * logic.  Called from non-static ShouldLoad().
+ */
+nsresult
+nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect,
+                                  uint32_t aContentType,
+                                  nsIURI* aContentLocation,
+                                  nsIURI* aRequestingLocation,
+                                  nsISupports* aRequestingContext,
+                                  const nsACString& aMimeGuess,
+                                  nsISupports* aExtra,
+                                  nsIPrincipal* aRequestPrincipal,
+                                  int16_t* aDecision)
+{
   // Asserting that we are on the main thread here and hence do not have to lock
   // and unlock sBlockMixedScript and sBlockMixedDisplay before reading/writing
   // to them.
   MOZ_ASSERT(NS_IsMainThread());
 
   // Assume active (high risk) content and blocked by default
   MixedContentTypes classification = eMixedScript;
   // Make decision to block/reject by default
@@ -437,18 +471,22 @@ nsMixedContentBlocker::ShouldLoad(uint32
   bool schemeSecure = false;
   if (NS_FAILED(NS_URIChainHasFlags(aContentLocation, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE , &schemeLocal))  ||
       NS_FAILED(NS_URIChainHasFlags(aContentLocation, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA, &schemeNoReturnData)) ||
       NS_FAILED(NS_URIChainHasFlags(aContentLocation, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT, &schemeInherits)) ||
       NS_FAILED(NS_URIChainHasFlags(aContentLocation, nsIProtocolHandler::URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT, &schemeSecure))) {
     *aDecision = REJECT_REQUEST;
     return NS_ERROR_FAILURE;
   }
-
-  if (schemeLocal || schemeNoReturnData || schemeInherits || schemeSecure) {
+  // TYPE_IMAGE redirects are cached based on the original URI, not the final
+  // destination and hence cache hits for images may not have the correct
+  // aContentLocation.  Check if the cached hit went through an http redirect,
+  // and if it did, we can't treat this as a secure subresource.
+  if (!aHadInsecureImageRedirect &&
+      (schemeLocal || schemeNoReturnData || schemeInherits || schemeSecure)) {
     *aDecision = ACCEPT;
      return NS_OK;
   }
 
   // Since there are cases where aRequestingLocation and aRequestPrincipal are
   // definitely not the owning document, we try to ignore them by extracting the
   // requestingLocation in the following order:
   // 1) from the aRequestingContext, either extracting
@@ -539,17 +577,17 @@ nsMixedContentBlocker::ShouldLoad(uint32
   nsCOMPtr<nsIDocShell> docShell = NS_CP_GetDocShellFromContext(aRequestingContext);
   NS_ENSURE_TRUE(docShell, NS_OK);
   bool rootHasSecureConnection = false;
   bool allowMixedContent = false;
   bool isRootDocShell = false;
   rv = docShell->GetAllowMixedContentAndConnectionData(&rootHasSecureConnection, &allowMixedContent, &isRootDocShell);
   if (NS_FAILED(rv)) {
     *aDecision = REJECT_REQUEST;
-     return rv;
+    return rv;
   }
 
 
   // Get the sameTypeRoot tree item from the docshell
   nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot;
   docShell->GetSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot));
   NS_ASSERTION(sameTypeRoot, "No root tree item from docshell!");
 
--- a/dom/security/nsMixedContentBlocker.h
+++ b/dom/security/nsMixedContentBlocker.h
@@ -20,25 +20,46 @@ enum MixedContentTypes {
   eMixedScript,
   // "Display" content, such as images, audio, video, and <a ping>
   eMixedDisplay
 };
 
 #include "nsIContentPolicy.h"
 #include "nsIChannel.h"
 #include "nsIChannelEventSink.h"
+#include "imgRequest.h"
 
 class nsMixedContentBlocker : public nsIContentPolicy,
                               public nsIChannelEventSink
 {
+private:
   virtual ~nsMixedContentBlocker();
 
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSICONTENTPOLICY
   NS_DECL_NSICHANNELEVENTSINK
 
   nsMixedContentBlocker();
+
+  /* Static version of ShouldLoad() that contains all the Mixed Content Blocker
+   * logic.  Called from non-static ShouldLoad().
+   * Called directly from imageLib when an insecure redirect exists in a cached
+   * image load.
+   * @param aHadInsecureImageRedirect
+   *        boolean flag indicating that an insecure redirect through http
+   *        occured when this image was initially loaded and cached.
+   * Remaining parameters are from nsIContentPolicy::ShouldLoad().
+   */
+  static nsresult ShouldLoad(bool aHadInsecureImageRedirect,
+                             uint32_t aContentType,
+                             nsIURI* aContentLocation,
+                             nsIURI* aRequestingLocation,
+                             nsISupports* aRequestingContext,
+                             const nsACString& aMimeGuess,
+                             nsISupports* aExtra,
+                             nsIPrincipal* aRequestPrincipal,
+                             int16_t* aDecision);
   static bool sBlockMixedScript;
   static bool sBlockMixedDisplay;
 };
 
 #endif /* nsMixedContentBlocker_h___ */
--- a/gfx/2d/Matrix.cpp
+++ b/gfx/2d/Matrix.cpp
@@ -119,74 +119,101 @@ Matrix4x4::TransformBounds(const Rect& a
   }
 
   return Rect(min_x, min_y, max_x - min_x, max_y - min_y);
 }
 
 Point4D ComputePerspectivePlaneIntercept(const Point4D& aFirst,
                                          const Point4D& aSecond)
 {
-  // FIXME: See bug 1035611
-  // Since we can't easily deal with points as w=0 (since we divide by w), we
-  // approximate this by finding a point with w just greater than 0. Unfortunately
-  // this is a tradeoff between accuracy and floating point precision.
+  // This function will always return a point with a w value of 0.
+  // The X, Y, and Z components will point towards an infinite vanishing
+  // point.
 
-  // We want to interpolate aFirst and aSecond to find a point as close to
-  // the positive side of the w=0 plane as possible.
+  // We want to interpolate aFirst and aSecond to find the point intersecting
+  // with the w=0 plane.
 
   // Since we know what we want the w component to be, we can rearrange the
   // interpolation equation and solve for t.
-  float w = 0.00001f;
-  float t = (w - aFirst.w) / (aSecond.w - aFirst.w);
+  float t = -aFirst.w / (aSecond.w - aFirst.w);
 
   // Use t to find the remainder of the components
   return aFirst + (aSecond - aFirst) * t;
 }
 
-Rect Matrix4x4::ProjectRectBounds(const Rect& aRect) const
+Rect Matrix4x4::ProjectRectBounds(const Rect& aRect, const Rect &aClip) const
 {
+  // This function must never return std::numeric_limits<Float>::max() or any
+  // other arbitrary large value in place of inifinity.  This often occurs when
+  // aRect is an inversed projection matrix or when aRect is transformed to be
+  // partly behind and in front of the camera (w=0 plane in homogenous
+  // coordinates) - See Bug 1035611
+
+  // Some call-sites will call RoundGfxRectToAppRect which clips both the
+  // extents and dimensions of the rect to be bounded by nscoord_MAX.
+  // If we return a Rect that, when converted to nscoords, has a width or height
+  // greater than nscoord_MAX, RoundGfxRectToAppRect will clip the overflow
+  // off both the min and max end of the rect after clipping the extents of the
+  // rect, resulting in a translation of the rect towards the infinite end.
+
+  // The bounds returned by ProjectRectBounds are expected to be clipped only on
+  // the edges beyond the bounds of the coordinate system; otherwise, the
+  // clipped bounding box would be smaller than the correct one and result
+  // bugs such as incorrect culling (eg. Bug 1073056)
+
+  // To address this without requiring all code to work in homogenous
+  // coordinates or interpret infinite values correctly, a specialized
+  // clipping function is integrated into ProjectRectBounds.
+
+  // Callers should pass an aClip value that represents the extents to clip
+  // the result to, in the same coordinate system as aRect.
   Point4D points[4];
 
   points[0] = ProjectPoint(aRect.TopLeft());
   points[1] = ProjectPoint(aRect.TopRight());
   points[2] = ProjectPoint(aRect.BottomRight());
   points[3] = ProjectPoint(aRect.BottomLeft());
 
   Float min_x = std::numeric_limits<Float>::max();
   Float min_y = std::numeric_limits<Float>::max();
   Float max_x = -std::numeric_limits<Float>::max();
   Float max_y = -std::numeric_limits<Float>::max();
 
-  bool foundPoint = false;
   for (int i=0; i<4; i++) {
     // Only use points that exist above the w=0 plane
     if (points[i].HasPositiveWCoord()) {
-      foundPoint = true;
-      Point point2d = points[i].As2DPoint();
-      min_x = min<Float>(point2d.x, min_x);
-      max_x = max<Float>(point2d.x, max_x);
-      min_y = min<Float>(point2d.y, min_y);
-      max_y = max<Float>(point2d.y, max_y);
+      Point point2d = aClip.ClampPoint(points[i].As2DPoint());
+      min_x = std::min<Float>(point2d.x, min_x);
+      max_x = std::max<Float>(point2d.x, max_x);
+      min_y = std::min<Float>(point2d.y, min_y);
+      max_y = std::max<Float>(point2d.y, max_y);
     }
 
     int next = (i == 3) ? 0 : i + 1;
     if (points[i].HasPositiveWCoord() != points[next].HasPositiveWCoord()) {
-      // If the line between two points crosses the w=0 plane, then interpolate a point
-      // as close to the w=0 plane as possible and use that instead.
+      // If the line between two points crosses the w=0 plane, then interpolate
+      // to find the point of intersection with the w=0 plane and use that
+      // instead.
       Point4D intercept = ComputePerspectivePlaneIntercept(points[i], points[next]);
-
-      Point point2d = intercept.As2DPoint();
-      min_x = min<Float>(point2d.x, min_x);
-      max_x = max<Float>(point2d.x, max_x);
-      min_y = min<Float>(point2d.y, min_y);
-      max_y = max<Float>(point2d.y, max_y);
+      // Since intercept.w will always be 0 here, we interpret x,y,z as a
+      // direction towards an infinite vanishing point.
+      if (intercept.x < 0.0f) {
+        min_x = aClip.x;
+      } else if (intercept.x > 0.0f) {
+        max_x = aClip.XMost();
+      }
+      if (intercept.y < 0.0f) {
+        min_y = aClip.y;
+      } else if (intercept.y > 0.0f) {
+        max_y = aClip.YMost();
+      }
     }
   }
 
-  if (!foundPoint) {
+  if (max_x <= min_x || max_y <= min_y) {
     return Rect(0, 0, 0, 0);
   }
 
   return Rect(min_x, min_y, max_x - min_x, max_y - min_y);
 }
 
 bool
 Matrix4x4::Invert()
--- a/gfx/2d/Matrix.h
+++ b/gfx/2d/Matrix.h
@@ -473,17 +473,17 @@ public:
 
     // Solving for z when z' = 0 gives us:
     float z = -(aPoint.x * _13 + aPoint.y * _23 + _43) / _33;
 
     // Compute the transformed point
     return *this * Point4D(aPoint.x, aPoint.y, z, 1);
   }
 
-  Rect ProjectRectBounds(const Rect& aRect) const;
+  Rect ProjectRectBounds(const Rect& aRect, const Rect &aClip) const;
 
   static Matrix4x4 From2D(const Matrix &aMatrix) {
     Matrix4x4 matrix;
     matrix._11 = aMatrix._11;
     matrix._12 = aMatrix._12;
     matrix._21 = aMatrix._21;
     matrix._22 = aMatrix._22;
     matrix._41 = aMatrix._31;
--- a/gfx/ipc/GfxMessageUtils.h
+++ b/gfx/ipc/GfxMessageUtils.h
@@ -907,22 +907,28 @@ template <>
 struct ParamTraits<mozilla::layers::EventRegions>
 {
   typedef mozilla::layers::EventRegions paramType;
 
   static void Write(Message* aMsg, const paramType& aParam)
   {
     WriteParam(aMsg, aParam.mHitRegion);
     WriteParam(aMsg, aParam.mDispatchToContentHitRegion);
+    WriteParam(aMsg, aParam.mNoActionRegion);
+    WriteParam(aMsg, aParam.mHorizontalPanRegion);
+    WriteParam(aMsg, aParam.mVerticalPanRegion);
   }
 
   static bool Read(const Message* aMsg, void** aIter, paramType* aResult)
   {
     return (ReadParam(aMsg, aIter, &aResult->mHitRegion) &&
-            ReadParam(aMsg, aIter, &aResult->mDispatchToContentHitRegion));
+            ReadParam(aMsg, aIter, &aResult->mDispatchToContentHitRegion) &&
+            ReadParam(aMsg, aIter, &aResult->mNoActionRegion) &&
+            ReadParam(aMsg, aIter, &aResult->mHorizontalPanRegion) &&
+            ReadParam(aMsg, aIter, &aResult->mVerticalPanRegion));
   }
 };
 
 struct MessageAndAttributeMap
 {
   Message* msg;
   const mozilla::gfx::AttributeMap& map;
 };
--- a/gfx/layers/LayersLogging.cpp
+++ b/gfx/layers/LayersLogging.cpp
@@ -147,16 +147,25 @@ AppendToString(std::stringstream& aStrea
 {
   aStream << pfx << "{";
   if (!e.mHitRegion.IsEmpty()) {
     AppendToString(aStream, e.mHitRegion, " hitregion=", "");
   }
   if (!e.mDispatchToContentHitRegion.IsEmpty()) {
     AppendToString(aStream, e.mDispatchToContentHitRegion, " dispatchtocontentregion=", "");
   }
+  if (!e.mNoActionRegion.IsEmpty()) {
+    AppendToString(aStream, e.mNoActionRegion, " NoActionRegion=","");
+  }
+  if (!e.mHorizontalPanRegion.IsEmpty()) {
+    AppendToString(aStream, e.mHorizontalPanRegion, " HorizontalPanRegion=", "");
+  }
+  if (!e.mVerticalPanRegion.IsEmpty()) {
+    AppendToString(aStream, e.mVerticalPanRegion, " VerticalPanRegion=", "");
+  }
   aStream << "}" << sfx;
 }
 
 void
 AppendToString(std::stringstream& aStream, const nsIntSize& sz,
                const char* pfx, const char* sfx)
 {
   aStream << pfx;
--- a/gfx/layers/LayersTypes.h
+++ b/gfx/layers/LayersTypes.h
@@ -153,19 +153,34 @@ struct LayerRenderState {
 enum class ScaleMode : int8_t {
   SCALE_NONE,
   STRETCH,
   SENTINEL
 // Unimplemented - PRESERVE_ASPECT_RATIO_CONTAIN
 };
 
 struct EventRegions {
+  // The hit region for a layer contains all areas on the layer that are
+  // sensitive to events. This region is an over-approximation and may
+  // contain regions that are not actually sensitive, but any such regions
+  // will be included in the mDispatchToContentHitRegion.
   nsIntRegion mHitRegion;
+  // The mDispatchToContentHitRegion for a layer contains all areas for
+  // which the main-thread must be consulted before responding to events.
+  // This region will be a subregion of mHitRegion.
   nsIntRegion mDispatchToContentHitRegion;
 
+  // The following regions represent the touch-action areas of this layer.
+  // All of these regions are approximations to the true region, but any
+  // variance between the approximation and the true region is guaranteed
+  // to be included in the mDispatchToContentHitRegion.
+  nsIntRegion mNoActionRegion;
+  nsIntRegion mHorizontalPanRegion;
+  nsIntRegion mVerticalPanRegion;
+
   EventRegions()
   {
   }
 
   explicit EventRegions(nsIntRegion aHitRegion)
     : mHitRegion(aHitRegion)
   {
   }
--- a/gfx/layers/composite/CompositableHost.cpp
+++ b/gfx/layers/composite/CompositableHost.cpp
@@ -104,17 +104,19 @@ CompositableHost::FromIPDLActor(PComposi
 }
 
 void
 CompositableHost::UseTextureHost(TextureHost* aTexture)
 {
   if (!aTexture) {
     return;
   }
-  aTexture->SetCompositor(GetCompositor());
+  if (GetCompositor()) {
+    aTexture->SetCompositor(GetCompositor());
+  }
 }
 
 void
 CompositableHost::UseComponentAlphaTextures(TextureHost* aTextureOnBlack,
                                             TextureHost* aTextureOnWhite)
 {
   MOZ_ASSERT(aTextureOnBlack && aTextureOnWhite);
   aTextureOnBlack->SetCompositor(GetCompositor());
--- a/gfx/layers/d3d9/CompositorD3D9.cpp
+++ b/gfx/layers/d3d9/CompositorD3D9.cpp
@@ -585,17 +585,16 @@ CompositorD3D9::Ready()
 {
   if (mDeviceManager) {
     if (EnsureSwapChain()) {
       // We don't need to call VerifyReadyForRendering because that is
       // called by mSwapChain->PrepareForRendering() via EnsureSwapChain().
       CheckResetCount();
       return true;
     }
-    FailedToResetDevice();
     return false;
   }
 
   NS_ASSERTION(!mCurrentRT && !mDefaultRT,
                "Shouldn't have any render targets around, they must be released before our device");
   mSwapChain = nullptr;
 
   mDeviceManager = gfxWindowsPlatform::GetPlatform()->GetD3D9DeviceManager();
@@ -609,19 +608,16 @@ CompositorD3D9::Ready()
     return true;
   }
   return false;
 }
 
 void
 CompositorD3D9::FailedToResetDevice() {
   mFailedResetAttempts += 1;
-  auto withoutAssertion = CriticalLog::DefaultOptions(false);
-  gfxCriticalError(withoutAssertion) << "[D3D9] Failed to re-create a D3D9 device, attempt "
-                                     << mFailedResetAttempts;
   // 10 is a totally arbitrary number that we may want to increase or decrease
   // depending on how things behave in the wild.
   if (mFailedResetAttempts > 10) {
     MOZ_CRASH("Unable to get a working D3D9 Compositor");
   }
 }
 
 void
--- a/gfx/tests/gtest/TestVsync.cpp
+++ b/gfx/tests/gtest/TestVsync.cpp
@@ -54,31 +54,38 @@ public:
   }
 
   bool DidGetVsyncNotification()
   {
     MonitorAutoLock lock(mVsyncMonitor);
     return mDidGetVsyncNotification;
   }
 
+  void ResetVsyncNotification()
+  {
+    MonitorAutoLock lock(mVsyncMonitor);
+    mDidGetVsyncNotification = false;
+  }
+
 private:
   bool mDidGetVsyncNotification;
 
 private:
   Monitor mVsyncMonitor;
 };
 
 class VsyncTester : public ::testing::Test {
 protected:
   explicit VsyncTester()
   {
     gfxPlatform::GetPlatform();
     gfxPrefs::GetSingleton();
     if (gfxPrefs::HardwareVsyncEnabled() ) {
       mVsyncSource = gfxPlatform::GetPlatform()->GetHardwareVsync();
+      MOZ_RELEASE_ASSERT(mVsyncSource);
     }
   }
 
   virtual ~VsyncTester()
   {
     mVsyncSource = nullptr;
   }
 
@@ -141,8 +148,68 @@ TEST_F(VsyncTester, CompositorGetVsyncNo
   ASSERT_TRUE(globalDisplay.IsVsyncEnabled());
 
   testVsyncObserver->WaitForVsyncNotification();
   ASSERT_TRUE(testVsyncObserver->DidGetVsyncNotification());
 
   vsyncDispatcher = nullptr;
   testVsyncObserver = nullptr;
 }
+
+// Test that if we have vsync enabled, the parent refresh driver should get notifications
+TEST_F(VsyncTester, ParentRefreshDriverGetVsyncNotifications)
+{
+  if (!gfxPrefs::HardwareVsyncEnabled() || !gfxPrefs::VsyncAlignedRefreshDriver()) {
+    return;
+  }
+
+  VsyncSource::Display& globalDisplay = mVsyncSource->GetGlobalDisplay();
+  globalDisplay.DisableVsync();
+  ASSERT_FALSE(globalDisplay.IsVsyncEnabled());
+
+  nsRefPtr<RefreshTimerVsyncDispatcher> vsyncDispatcher = globalDisplay.GetRefreshTimerVsyncDispatcher();
+  ASSERT_TRUE(vsyncDispatcher != nullptr);
+
+  nsRefPtr<TestVsyncObserver> testVsyncObserver = new TestVsyncObserver();
+  vsyncDispatcher->SetParentRefreshTimer(testVsyncObserver);
+  ASSERT_TRUE(globalDisplay.IsVsyncEnabled());
+
+  testVsyncObserver->WaitForVsyncNotification();
+  ASSERT_TRUE(testVsyncObserver->DidGetVsyncNotification());
+  vsyncDispatcher->SetParentRefreshTimer(nullptr);
+
+  testVsyncObserver->ResetVsyncNotification();
+  testVsyncObserver->WaitForVsyncNotification();
+  ASSERT_FALSE(testVsyncObserver->DidGetVsyncNotification());
+
+  vsyncDispatcher = nullptr;
+  testVsyncObserver = nullptr;
+}
+
+// Test that child refresh vsync observers get vsync notifications
+TEST_F(VsyncTester, ChildRefreshDriverGetVsyncNotifications)
+{
+  if (!gfxPrefs::HardwareVsyncEnabled() || !gfxPrefs::VsyncAlignedRefreshDriver()) {
+    return;
+  }
+
+  VsyncSource::Display& globalDisplay = mVsyncSource->GetGlobalDisplay();
+  globalDisplay.DisableVsync();
+  ASSERT_FALSE(globalDisplay.IsVsyncEnabled());
+
+  nsRefPtr<RefreshTimerVsyncDispatcher> vsyncDispatcher = globalDisplay.GetRefreshTimerVsyncDispatcher();
+  ASSERT_TRUE(vsyncDispatcher != nullptr);
+
+  nsRefPtr<TestVsyncObserver> testVsyncObserver = new TestVsyncObserver();
+  vsyncDispatcher->AddChildRefreshTimer(testVsyncObserver);
+  ASSERT_TRUE(globalDisplay.IsVsyncEnabled());
+
+  testVsyncObserver->WaitForVsyncNotification();
+  ASSERT_TRUE(testVsyncObserver->DidGetVsyncNotification());
+
+  vsyncDispatcher->RemoveChildRefreshTimer(testVsyncObserver);
+  testVsyncObserver->ResetVsyncNotification();
+  testVsyncObserver->WaitForVsyncNotification();
+  ASSERT_FALSE(testVsyncObserver->DidGetVsyncNotification());
+
+  vsyncDispatcher = nullptr;
+  testVsyncObserver = nullptr;
+}
--- a/image/src/imgLoader.cpp
+++ b/image/src/imgLoader.cpp
@@ -10,30 +10,32 @@
 
 #include "ImageLogging.h"
 #include "nsPrintfCString.h"
 #include "imgLoader.h"
 #include "imgRequestProxy.h"
 
 #include "nsCOMPtr.h"
 
+#include "nsContentPolicyUtils.h"
 #include "nsContentUtils.h"
 #include "nsCORSListenerProxy.h"
 #include "nsNetUtil.h"
 #include "nsMimeTypes.h"
 #include "nsStreamUtils.h"
 #include "nsIHttpChannel.h"
 #include "nsICachingChannel.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIProgressEventSink.h"
 #include "nsIChannelEventSink.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsIFileURL.h"
 #include "nsCRT.h"
 #include "nsINetworkPredictor.h"
+#include "mozilla/dom/nsMixedContentBlocker.h"
 
 #include "nsIApplicationCache.h"
 #include "nsIApplicationCacheContainer.h"
 
 #include "nsIMemoryReporter.h"
 #include "Image.h"
 #include "gfxPrefs.h"
 
@@ -583,24 +585,89 @@ static bool ShouldRevalidateEntry(imgCac
     else if (!(aFlags & nsIRequest::LOAD_FROM_CACHE)) {
       bValidateEntry = true;
     }
   }
 
   return bValidateEntry;
 }
 
+/* Call content policies on cached images that went through a redirect */
+static bool
+ShouldLoadCachedImage(imgRequest* aImgRequest,
+                      nsISupports* aLoadingContext,
+                      nsIPrincipal* aLoadingPrincipal)
+{
+  /* Call content policies on cached images - Bug 1082837
+   * Cached images are keyed off of the first uri in a redirect chain.
+   * Hence content policies don't get a chance to test the intermediate hops
+   * or the final desitnation.  Here we test the final destination using
+   * mCurrentURI off of the imgRequest and passing it into content policies.
+   * For Mixed Content Blocker, we do an additional check to determine if any
+   * of the intermediary hops went through an insecure redirect with the
+   * mHadInsecureRedirect flag
+   */
+  bool insecureRedirect = aImgRequest->HadInsecureRedirect();
+  nsCOMPtr<nsIURI> contentLocation;
+  aImgRequest->GetCurrentURI(getter_AddRefs(contentLocation));
+  nsresult rv;
+
+  int16_t decision = nsIContentPolicy::REJECT_REQUEST;
+  rv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_IMAGE,
+                                 contentLocation,
+                                 aLoadingPrincipal,
+                                 aLoadingContext,
+                                 EmptyCString(), //mime guess
+                                 nullptr, //aExtra
+                                 &decision,
+                                 nsContentUtils::GetContentPolicy(),
+                                 nsContentUtils::GetSecurityManager());
+  if (NS_FAILED(rv) || !NS_CP_ACCEPTED(decision)) {
+    return false;
+  }
+
+  // We call all Content Policies above, but we also have to call mcb
+  // individually to check the intermediary redirect hops are secure.
+  if (insecureRedirect) {
+    if (!nsContentUtils::IsSystemPrincipal(aLoadingPrincipal)) {
+      // Set the requestingLocation from the aLoadingPrincipal.
+      nsCOMPtr<nsIURI> requestingLocation;
+      if (aLoadingPrincipal) {
+        rv = aLoadingPrincipal->GetURI(getter_AddRefs(requestingLocation));
+        NS_ENSURE_SUCCESS(rv, false);
+      }
+
+      // reset the decision for mixed content blocker check
+      decision = nsIContentPolicy::REJECT_REQUEST;
+      rv = nsMixedContentBlocker::ShouldLoad(insecureRedirect,
+                                             nsIContentPolicy::TYPE_IMAGE,
+                                             contentLocation,
+                                             requestingLocation,
+                                             aLoadingContext,
+                                             EmptyCString(), //mime guess
+                                             nullptr,
+                                             aLoadingPrincipal,
+                                             &decision);
+      if (NS_FAILED(rv) || !NS_CP_ACCEPTED(decision)) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+}
+
 // Returns true if this request is compatible with the given CORS mode on the
 // given loading principal, and false if the request may not be reused due
 // to CORS.  Also checks the Referrer Policy, since requests with different
 // referrers/policies may generate different responses.
 static bool
 ValidateSecurityInfo(imgRequest* request, bool forcePrincipalCheck,
                      int32_t corsmode, nsIPrincipal* loadingPrincipal,
-                     ReferrerPolicy referrerPolicy)
+                     nsISupports* aCX, ReferrerPolicy referrerPolicy)
 {
   // If the entry's Referrer Policy doesn't match, we can't use this request.
   if (referrerPolicy != request->GetReferrerPolicy()) {
     return false;
   }
 
   // If the entry's CORS mode doesn't match, or the CORS mode matches but the
   // document principal isn't the same, we can't use this request.
@@ -614,21 +681,24 @@ ValidateSecurityInfo(imgRequest* request
     // request.
     if (otherprincipal && !loadingPrincipal) {
       return false;
     }
 
     if (otherprincipal && loadingPrincipal) {
       bool equals = false;
       otherprincipal->Equals(loadingPrincipal, &equals);
-      return equals;
+      if (!equals) {
+        return false;
+      }
     }
   }
 
-  return true;
+  // Content Policy Check on Cached Images
+  return ShouldLoadCachedImage(request, aCX, loadingPrincipal);
 }
 
 static nsresult NewImageChannel(nsIChannel **aResult,
                                 // If aForcePrincipalCheckForCacheEntry is
                                 // true, then we will force a principal check
                                 // even when not using CORS before assuming we
                                 // have a cache hit on a cache entry that we
                                 // create for this channel.  This is an out
@@ -1599,17 +1669,17 @@ bool imgLoader::ValidateEntry(imgCacheEn
 
   nsRefPtr<imgRequest> request(aEntry->GetRequest());
 
   if (!request)
     return false;
 
   if (!ValidateSecurityInfo(request, aEntry->ForcePrincipalCheck(),
                             aCORSMode, aLoadingPrincipal,
-                            aReferrerPolicy))
+                            aCX, aReferrerPolicy))
     return false;
 
   // data URIs are immutable and by their nature can't leak data, so we can
   // just return true in that case.  Doing so would mean that shift-reload
   // doesn't reload data URI documents/images though (which is handy for
   // debugging during gecko development) so we make an exception in that case.
   nsAutoCString scheme;
   aURI->GetScheme(scheme);
@@ -2033,17 +2103,18 @@ nsresult imgLoader::LoadImage(nsIURI *aU
 
     NewRequestAndEntry(forcePrincipalCheck, this, getter_AddRefs(request), getter_AddRefs(entry));
 
     PR_LOG(GetImgLog(), PR_LOG_DEBUG,
            ("[this=%p] imgLoader::LoadImage -- Created new imgRequest [request=%p]\n", this, request.get()));
 
     nsCOMPtr<nsILoadGroup> channelLoadGroup;
     newChannel->GetLoadGroup(getter_AddRefs(channelLoadGroup));
-    request->Init(aURI, aURI, channelLoadGroup, newChannel, entry, aCX,
+    request->Init(aURI, aURI, /* aHadInsecureRedirect = */ false,
+                  channelLoadGroup, newChannel, entry, aCX,
                   aLoadingPrincipal, corsmode, aReferrerPolicy);
 
     // Add the initiator type for this image load
     nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(newChannel);
     if (timedChannel) {
       timedChannel->SetInitiatorType(initiatorType);
     }
 
@@ -2261,18 +2332,26 @@ nsresult imgLoader::LoadImageWithChannel
     // inherited on the channel.
     NewRequestAndEntry(true, this, getter_AddRefs(request), getter_AddRefs(entry));
 
     // We use originalURI here to fulfil the imgIRequest contract on GetURI.
     nsCOMPtr<nsIURI> originalURI;
     channel->GetOriginalURI(getter_AddRefs(originalURI));
 
     // No principal specified here, because we're not passed one.
-    request->Init(originalURI, uri, channel, channel, entry,
-                  aCX, nullptr, imgIRequest::CORS_NONE, RP_Default);
+    // In LoadImageWithChannel, the redirects that may have been
+    // assoicated with this load would have gone through necko.
+    // We only have the final URI in ImageLib and hence don't know
+    // if the request went through insecure redirects.  But if it did,
+    // the necko cache should have handled that (since all necko cache hits
+    // including the redirects will go through content policy).  Hence, we
+    // can set aHadInsecureRedirect to false here.
+    request->Init(originalURI, uri, /* aHadInsecureRedirect = */ false,
+                  channel, channel, entry, aCX, nullptr,
+                  imgIRequest::CORS_NONE, RP_Default);
 
     nsRefPtr<ProxyListener> pl =
       new ProxyListener(static_cast<nsIStreamListener*>(request.get()));
     pl.forget(listener);
 
 
     // Try to add the new request into the cache.
     PutIntoCache(originalURI, entry);
@@ -2510,17 +2589,18 @@ NS_IMPL_ISUPPORTS(imgCacheValidator, nsI
 
 imgCacheValidator::imgCacheValidator(nsProgressNotificationProxy* progress,
                                      imgLoader* loader, imgRequest *request,
                                      nsISupports* aContext,
                                      bool forcePrincipalCheckForCacheEntry)
  : mProgressProxy(progress),
    mRequest(request),
    mContext(aContext),
-   mImgLoader(loader)
+   mImgLoader(loader),
+   mHadInsecureRedirect(false)
 {
   NewRequestAndEntry(forcePrincipalCheckForCacheEntry, loader, getter_AddRefs(mNewRequest),
                      getter_AddRefs(mNewEntry));
 }
 
 imgCacheValidator::~imgCacheValidator()
 {
   if (mRequest) {
@@ -2617,19 +2697,18 @@ NS_IMETHODIMP imgCacheValidator::OnStart
   mRequest->RemoveFromCache();
 
   mRequest->SetValidator(nullptr);
   mRequest = nullptr;
 
   // We use originalURI here to fulfil the imgIRequest contract on GetURI.
   nsCOMPtr<nsIURI> originalURI;
   channel->GetOriginalURI(getter_AddRefs(originalURI));
-  mNewRequest->Init(originalURI, uri, aRequest, channel, mNewEntry,
-                    mContext, loadingPrincipal,
-                    corsmode, refpol);
+  mNewRequest->Init(originalURI, uri, mHadInsecureRedirect, aRequest, channel,
+                    mNewEntry, mContext, loadingPrincipal, corsmode, refpol);
 
   mDestListener = new ProxyListener(mNewRequest);
 
   // Try to add the new request into the cache. Note that the entry must be in
   // the cache before the proxies' ownership changes, because adding a proxy
   // changes the caching behaviour for imgRequests.
   mImgLoader->PutIntoCache(originalURI, mNewEntry);
 
@@ -2713,16 +2792,31 @@ NS_IMETHODIMP imgCacheValidator::GetInte
 /** nsIChannelEventSink methods **/
 NS_IMETHODIMP imgCacheValidator::AsyncOnChannelRedirect(nsIChannel *oldChannel,
                                                         nsIChannel *newChannel, uint32_t flags,
                                                         nsIAsyncVerifyRedirectCallback *callback)
 {
   // Note all cache information we get from the old channel.
   mNewRequest->SetCacheValidation(mNewEntry, oldChannel);
 
+  // If the previous URI is a non-HTTPS URI, record that fact for later use by
+  // security code, which needs to know whether there is an insecure load at any
+  // point in the redirect chain.
+  nsCOMPtr<nsIURI> oldURI;
+  bool isHttps = false;
+  bool isChrome = false;
+  bool schemeLocal = false;
+  if (NS_FAILED(oldChannel->GetURI(getter_AddRefs(oldURI))) ||
+      NS_FAILED(oldURI->SchemeIs("https", &isHttps)) ||
+      NS_FAILED(oldURI->SchemeIs("chrome", &isChrome)) ||
+      NS_FAILED(NS_URIChainHasFlags(oldURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE , &schemeLocal))  ||
+      (!isHttps && !isChrome && !schemeLocal)) {
+    mHadInsecureRedirect = true;
+  }
+
   // Prepare for callback
   mRedirectCallback = callback;
   mRedirectChannel = newChannel;
 
   return mProgressProxy->AsyncOnChannelRedirect(oldChannel, newChannel, flags, this);
 }
 
 NS_IMETHODIMP imgCacheValidator::OnRedirectVerifyCallback(nsresult aResult)
--- a/image/src/imgLoader.h
+++ b/image/src/imgLoader.h
@@ -522,11 +522,13 @@ private:
   nsCOMArray<imgIRequest> mProxies;
 
   nsRefPtr<imgRequest> mNewRequest;
   nsRefPtr<imgCacheEntry> mNewEntry;
 
   nsCOMPtr<nsISupports> mContext;
 
   imgLoader* mImgLoader;
+
+  bool mHadInsecureRedirect;
 };
 
 #endif  // imgLoader_h__
--- a/image/src/imgRequest.cpp
+++ b/image/src/imgRequest.cpp
@@ -72,16 +72,17 @@ imgRequest::imgRequest(imgLoader* aLoade
  , mImageErrorCode(NS_OK)
  , mMutex("imgRequest")
  , mProgressTracker(new ProgressTracker())
  , mIsMultiPartChannel(false)
  , mGotData(false)
  , mIsInCache(false)
  , mDecodeRequested(false)
  , mNewPartPending(false)
+ , mHadInsecureRedirect(false)
 { }
 
 imgRequest::~imgRequest()
 {
   if (mLoader) {
     mLoader->RemoveFromUncachedImages(this);
   }
   if (mURI) {
@@ -89,16 +90,17 @@ imgRequest::~imgRequest()
     mURI->GetSpec(spec);
     LOG_FUNC_WITH_PARAM(GetImgLog(), "imgRequest::~imgRequest()", "keyuri", spec.get());
   } else
     LOG_FUNC(GetImgLog(), "imgRequest::~imgRequest()");
 }
 
 nsresult imgRequest::Init(nsIURI *aURI,
                           nsIURI *aCurrentURI,
+                          bool aHadInsecureRedirect,
                           nsIRequest *aRequest,
                           nsIChannel *aChannel,
                           imgCacheEntry *aCacheEntry,
                           nsISupports* aCX,
                           nsIPrincipal* aLoadingPrincipal,
                           int32_t aCORSMode,
                           ReferrerPolicy aReferrerPolicy)
 {
@@ -120,16 +122,36 @@ nsresult imgRequest::Init(nsIURI *aURI,
   mRequest = aRequest;
   mChannel = aChannel;
   mTimedChannel = do_QueryInterface(mChannel);
 
   mLoadingPrincipal = aLoadingPrincipal;
   mCORSMode = aCORSMode;
   mReferrerPolicy = aReferrerPolicy;
 
+  // If the original URI and the current URI are different, check whether the
+  // original URI is secure. We deliberately don't take the current URI into
+  // account, as it needs to be handled using more complicated rules than
+  // earlier elements of the redirect chain.
+  if (aURI != aCurrentURI) {
+    bool isHttps = false;
+    bool isChrome = false;
+    bool schemeLocal = false;
+    if (NS_FAILED(aURI->SchemeIs("https", &isHttps)) ||
+        NS_FAILED(aURI->SchemeIs("chrome", &isChrome)) ||
+        NS_FAILED(NS_URIChainHasFlags(aURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE , &schemeLocal))  ||
+        (!isHttps && !isChrome && !schemeLocal)) {
+      mHadInsecureRedirect = true;
+    }
+  }
+
+  // imgCacheValidator may have handled redirects before we were created, so we
+  // allow the caller to let us know if any redirects were insecure.
+  mHadInsecureRedirect = mHadInsecureRedirect || aHadInsecureRedirect;
+
   mChannel->GetNotificationCallbacks(getter_AddRefs(mPrevChannelSink));
 
   NS_ASSERTION(mPrevChannelSink != this,
                "Initializing with a channel that already calls back to us!");
 
   mChannel->SetNotificationCallbacks(this);
 
   mCacheEntry = aCacheEntry;
@@ -1148,27 +1170,41 @@ imgRequest::OnRedirectVerifyCallback(nsr
 
   if (LOG_TEST(PR_LOG_DEBUG)) {
     nsAutoCString spec;
     if (mCurrentURI)
       mCurrentURI->GetSpec(spec);
     LOG_MSG_WITH_PARAM(GetImgLog(), "imgRequest::OnChannelRedirect", "old", spec.get());
   }
 
-  // make sure we have a protocol that returns data rather than opens
-  // an external application, e.g. mailto:
+  // If the previous URI is a non-HTTPS URI, record that fact for later use by
+  // security code, which needs to know whether there is an insecure load at any
+  // point in the redirect chain.
+  bool isHttps = false;
+  bool isChrome = false;
+  bool schemeLocal = false;
+  if (NS_FAILED(mCurrentURI->SchemeIs("https", &isHttps)) ||
+      NS_FAILED(mCurrentURI->SchemeIs("chrome", &isChrome)) ||
+      NS_FAILED(NS_URIChainHasFlags(mCurrentURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE , &schemeLocal))  ||
+      (!isHttps && !isChrome && !schemeLocal)) {
+    mHadInsecureRedirect = true;
+  }
+
+  // Update the current URI.
   mChannel->GetURI(getter_AddRefs(mCurrentURI));
 
   if (LOG_TEST(PR_LOG_DEBUG)) {
     nsAutoCString spec;
     if (mCurrentURI)
       mCurrentURI->GetSpec(spec);
     LOG_MSG_WITH_PARAM(GetImgLog(), "imgRequest::OnChannelRedirect", "new", spec.get());
   }
 
+  // Make sure we have a protocol that returns data rather than opens an
+  // external application, e.g. 'mailto:'.
   bool doesNotReturnData = false;
   nsresult rv =
     NS_URIChainHasFlags(mCurrentURI, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA,
                         &doesNotReturnData);
 
   if (NS_SUCCEEDED(rv) && doesNotReturnData)
     rv = NS_ERROR_ABORT;
 
--- a/image/src/imgRequest.h
+++ b/image/src/imgRequest.h
@@ -61,16 +61,17 @@ public:
   NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
   NS_DECL_NSIREQUESTOBSERVER
   NS_DECL_NSICHANNELEVENTSINK
   NS_DECL_NSIINTERFACEREQUESTOR
   NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK
 
   nsresult Init(nsIURI *aURI,
                 nsIURI *aCurrentURI,
+                bool aHadInsecureRedirect,
                 nsIRequest *aRequest,
                 nsIChannel *aChannel,
                 imgCacheEntry *aCacheEntry,
                 nsISupports* aCX,
                 nsIPrincipal* aLoadingPrincipal,
                 int32_t aCORSMode,
                 ReferrerPolicy aReferrerPolicy);
 
@@ -108,16 +109,20 @@ public:
   // Check if application cache of the original load is different from
   // application cache of the new load.  Also lack of application cache
   // on one of the loads is considered a change of a loading cache since
   // HTTP cache may contain a different data then app cache.
   bool CacheChanged(nsIRequest* aNewRequest);
 
   bool GetMultipart() const;
 
+  // Returns whether we went through an insecure (non-HTTPS) redirect at some
+  // point during loading. This does not consider the current URI.
+  bool HadInsecureRedirect() const { return mHadInsecureRedirect; }
+
   // The CORS mode for which we loaded this image.
   int32_t GetCORSMode() const { return mCORSMode; }
 
   // The Referrer Policy in effect when loading this image.
   ReferrerPolicy GetReferrerPolicy() const { return mReferrerPolicy; }
 
   // The principal for the document that loaded this image. Used when trying to
   // validate a CORS image load.
@@ -263,11 +268,12 @@ private:
   // must not be a part of this bitfield.
   nsRefPtr<ProgressTracker> mProgressTracker;
   nsRefPtr<Image> mImage;
   bool mIsMultiPartChannel : 1;
   bool mGotData : 1;
   bool mIsInCache : 1;
   bool mDecodeRequested : 1;
   bool mNewPartPending : 1;
+  bool mHadInsecureRedirect : 1;
 };
 
 #endif
--- a/js/src/asmjs/AsmJSSignalHandlers.cpp
+++ b/js/src/asmjs/AsmJSSignalHandlers.cpp
@@ -1333,19 +1333,8 @@ js::InterruptRunningJitCode(JSRuntime *r
 #else
     // On Unix, we instead deliver an async signal to the main thread which
     // halts the thread and callers our JitInterruptHandler (which has already
     // been installed by EnsureSignalHandlersInstalled).
     pthread_t thread = (pthread_t)rt->ownerThreadNative();
     pthread_kill(thread, sInterruptSignal);
 #endif
 }
-
-// This is not supported by clang-cl yet.
-#if defined(MOZ_ASAN) && defined(JS_STANDALONE) && !defined(_MSC_VER)
-// Usually, this definition is found in mozglue (see mozglue/build/AsanOptions.cpp).
-// However, when doing standalone JS builds, mozglue is not used and we must ensure
-// that we still allow custom SIGSEGV handlers for asm.js and ion to work correctly.
-extern "C" MOZ_ASAN_BLACKLIST
-const char* __asan_default_options() {
-    return "allow_user_segv_handler=1";
-}
-#endif
--- a/js/src/gc/GCTrace.cpp
+++ b/js/src/gc/GCTrace.cpp
@@ -94,17 +94,17 @@ js::gc::InitTrace(GCRuntime &gc)
     if (!gcTraceFile) {
         FinishTrace();
         return false;
     }
 
     TraceEvent(TraceEventInit, 0, TraceFormatVersion);
 
     /* Trace information about thing sizes. */
-    for (ALL_ALLOC_KINDS(kind))
+    for (auto kind : AllAllocKinds())
         TraceEvent(TraceEventThingSize, Arena::thingSize(kind));
 
     return true;
 }
 
 void
 js::gc::FinishTrace()
 {
--- a/js/src/gc/Heap.h
+++ b/js/src/gc/Heap.h
@@ -6,16 +6,17 @@
 
 #ifndef gc_Heap_h
 #define gc_Heap_h
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Atomics.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/EnumeratedArray.h"
+#include "mozilla/EnumeratedRange.h"
 #include "mozilla/PodOperations.h"
 
 #include <stddef.h>
 #include <stdint.h>
 
 #include "jspubtd.h"
 #include "jstypes.h"
 #include "jsutil.h"
@@ -107,21 +108,27 @@ enum class AllocKind {
     JITCODE,
     LIMIT,
     LAST = LIMIT - 1
 };
 
 static_assert(uint8_t(AllocKind::OBJECT0) == 0, "Please check AllocKind iterations and comparisons"
     " of the form |kind <= AllocKind::OBJECT_LAST| to ensure their range is still valid!");
 
-#define ALL_ALLOC_KINDS(i) AllocKind i = AllocKind::FIRST;\
-    i < AllocKind::LIMIT; i = AllocKind(uint8_t(i) + 1)
+inline decltype(mozilla::MakeEnumeratedRange<int>(AllocKind::FIRST, AllocKind::LIMIT))
+AllAllocKinds()
+{
+    return mozilla::MakeEnumeratedRange<int>(AllocKind::FIRST, AllocKind::LIMIT);
+}
 
-#define OBJECT_ALLOC_KINDS(i) AllocKind i = AllocKind::OBJECT0;\
-    i < AllocKind::OBJECT_LIMIT; i = AllocKind(uint8_t(i) + 1)
+inline decltype(mozilla::MakeEnumeratedRange<int>(AllocKind::OBJECT0, AllocKind::OBJECT_LIMIT))
+ObjectAllocKinds()
+{
+    return mozilla::MakeEnumeratedRange<int>(AllocKind::OBJECT0, AllocKind::OBJECT_LIMIT);
+}
 
 template<typename ValueType> using AllAllocKindArray =
     mozilla::EnumeratedArray<AllocKind, AllocKind::LIMIT, ValueType>;
 
 template<typename ValueType> using ObjectAllocKindArray =
     mozilla::EnumeratedArray<AllocKind, AllocKind::OBJECT_LIMIT, ValueType>;
 
 static inline JSGCTraceKind
--- a/js/src/gc/Iteration.cpp
+++ b/js/src/gc/Iteration.cpp
@@ -33,17 +33,17 @@ static void
 IterateCompartmentsArenasCells(JSRuntime *rt, Zone *zone, void *data,
                                JSIterateCompartmentCallback compartmentCallback,
                                IterateArenaCallback arenaCallback,
                                IterateCellCallback cellCallback)
 {
     for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next())
         (*compartmentCallback)(rt, data, comp);
 
-    for (ALL_ALLOC_KINDS(thingKind)) {
+    for (auto thingKind : AllAllocKinds()) {
         JSGCTraceKind traceKind = MapAllocToTraceKind(thingKind);
         size_t thingSize = Arena::thingSize(thingKind);
 
         for (ArenaIter aiter(zone, thingKind); !aiter.done(); aiter.next()) {
             ArenaHeader *aheader = aiter.get();
             (*arenaCallback)(rt, data, aheader->getArena(), traceKind, thingSize);
             for (ArenaCellIterUnderGC iter(aheader); !iter.done(); iter.next())
                 (*cellCallback)(rt, data, iter.getCell(), traceKind, thingSize);
@@ -112,17 +112,17 @@ js::IterateScripts(JSRuntime *rt, JSComp
 }
 
 void
 js::IterateGrayObjects(Zone *zone, GCThingCallback cellCallback, void *data)
 {
     zone->runtimeFromMainThread()->gc.evictNursery();
     AutoPrepareForTracing prep(zone->runtimeFromMainThread(), SkipAtoms);
 
-    for (OBJECT_ALLOC_KINDS(thingKind)) {
+    for (auto thingKind : ObjectAllocKinds()) {
         for (ZoneCellIterUnderGC i(zone, thingKind); !i.done(); i.next()) {
             JSObject *obj = i.get<JSObject>();
             if (obj->asTenured().isMarked(GRAY))
                 cellCallback(data, JS::GCCellPtr(obj));
         }
     }
 }
 
--- a/js/src/gc/Verifier.cpp
+++ b/js/src/gc/Verifier.cpp
@@ -496,17 +496,17 @@ js::gc::GCRuntime::endVerifyPostBarriers
     if (!edges.init())
         goto oom;
     trc->edges = &edges;
     storeBuffer.markAll(trc);
 
     /* Walk the heap to find any edges not the the |edges| set. */
     trc->setTraceCallback(PostVerifierVisitEdge);
     for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) {
-        for (ALL_ALLOC_KINDS(kind)) {
+        for (auto kind : AllAllocKinds()) {
             for (ZoneCellIterUnderGC cells(zone, kind); !cells.done(); cells.next()) {
                 Cell *src = cells.getCell();
                 JS_TraceChildren(trc, src, MapAllocToTraceKind(kind));
             }
         }
     }
 
 oom:
--- a/js/src/jit/PerfSpewer.cpp
+++ b/js/src/jit/PerfSpewer.cpp
@@ -1,16 +1,18 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * 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 "jit/PerfSpewer.h"
 
+#include "mozilla/IntegerPrintfMacros.h"
+
 #if defined(__linux__)
 # include <unistd.h>
 #endif
 
 #ifdef JS_ION_PERF
 # include "jit/JitSpewer.h"
 # include "jit/LinearScan.h"
 # include "jit/LIR.h"
--- a/js/src/jit/mips/MacroAssembler-mips.h
+++ b/js/src/jit/mips/MacroAssembler-mips.h
@@ -1215,17 +1215,17 @@ public:
 
     void callWithExitFrame(Label *target);
     void callWithExitFrame(JitCode *target);
     void callWithExitFrame(JitCode *target, Register dynStack);
 
     // Makes a call using the only two methods that it is sane for indep code
     // to make a call.
     void callJit(Register callee);
-    void callJitFromAsmJS(Register callee) { call(callee); }
+    void callJitFromAsmJS(Register callee) { callJit(callee); }
 
     void reserveStack(uint32_t amount);
     void freeStack(uint32_t amount);
     void freeStack(Register amount);
 
     void add32(Register src, Register dest);
     void add32(Imm32 imm, Register dest);
     void add32(Imm32 imm, const Address &dest);
@@ -1330,17 +1330,17 @@ public:
     void loadUnalignedInt32x4(const Address &addr, FloatRegister dest) { MOZ_CRASH("NYI"); }
     void loadUnalignedInt32x4(const BaseIndex &addr, FloatRegister dest) { MOZ_CRASH("NYI"); }
     void storeUnalignedInt32x4(FloatRegister src, Address addr) { MOZ_CRASH("NYI"); }
     void storeUnalignedInt32x4(FloatRegister src, BaseIndex addr) { MOZ_CRASH("NYI"); }
 
     void loadFloat32x3(const Address &src, FloatRegister dest) { MOZ_CRASH("NYI"); }
     void loadFloat32x3(const BaseIndex &src, FloatRegister dest) { MOZ_CRASH("NYI"); }
     void storeFloat32x3(FloatRegister src, const Address &dest) { MOZ_CRASH("NYI"); }
-    void storeFloat32x(FloatRegister src, const BaseIndex &dest) { MOZ_CRASH("NYI"); }
+    void storeFloat32x3(FloatRegister src, const BaseIndex &dest) { MOZ_CRASH("NYI"); }
     void loadAlignedFloat32x4(const Address &addr, FloatRegister dest) { MOZ_CRASH("NYI"); }
     void storeAlignedFloat32x4(FloatRegister src, Address addr) { MOZ_CRASH("NYI"); }
     void loadUnalignedFloat32x4(const Address &addr, FloatRegister dest) { MOZ_CRASH("NYI"); }
     void loadUnalignedFloat32x4(const BaseIndex &addr, FloatRegister dest) { MOZ_CRASH("NYI"); }
     void storeUnalignedFloat32x4(FloatRegister src, Address addr) { MOZ_CRASH("NYI"); }
     void storeUnalignedFloat32x4(FloatRegister src, BaseIndex addr) { MOZ_CRASH("NYI"); }
 
     void loadDouble(const Address &addr, FloatRegister dest);
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -227,16 +227,17 @@ MSG_DEF(JSMSG_CURLY_BEFORE_CATCH,      0
 MSG_DEF(JSMSG_CURLY_BEFORE_CLASS,      0, JSEXN_SYNTAXERR, "missing { before class body")
 MSG_DEF(JSMSG_CURLY_BEFORE_FINALLY,    0, JSEXN_SYNTAXERR, "missing { before finally block")
 MSG_DEF(JSMSG_CURLY_BEFORE_SWITCH,     0, JSEXN_SYNTAXERR, "missing { before switch body")
 MSG_DEF(JSMSG_CURLY_BEFORE_TRY,        0, JSEXN_SYNTAXERR, "missing { before try block")
 MSG_DEF(JSMSG_CURLY_IN_COMPOUND,       0, JSEXN_SYNTAXERR, "missing } in compound statement")
 MSG_DEF(JSMSG_DECLARATION_AFTER_EXPORT,0, JSEXN_SYNTAXERR, "missing declaration after 'export' keyword")
 MSG_DEF(JSMSG_DECLARATION_AFTER_IMPORT,0, JSEXN_SYNTAXERR, "missing declaration after 'import' keyword")
 MSG_DEF(JSMSG_DEPRECATED_DELETE_OPERAND, 0, JSEXN_SYNTAXERR, "applying the 'delete' operator to an unqualified name is deprecated")
+MSG_DEF(JSMSG_DEPRECATED_FLAGS_ARG,    0, JSEXN_NONE, "flags argument of String.prototype.{search,match,replace} is deprecated")
 MSG_DEF(JSMSG_DEPRECATED_LET_BLOCK,      0, JSEXN_NONE, "JavaScript 1.7's let blocks are deprecated")
 MSG_DEF(JSMSG_DEPRECATED_FOR_EACH,     0, JSEXN_NONE, "JavaScript 1.6's for-each-in loops are deprecated; consider using ES6 for-of instead")
 MSG_DEF(JSMSG_DEPRECATED_LET_EXPRESSION, 0, JSEXN_NONE, "JavaScript 1.7's let expressions are deprecated")
 MSG_DEF(JSMSG_DEPRECATED_OCTAL,        0, JSEXN_SYNTAXERR, "octal literals and octal escape sequences are deprecated")
 MSG_DEF(JSMSG_DEPRECATED_PRAGMA,       1, JSEXN_NONE, "Using //@ to indicate {0} pragmas is deprecated. Use //# instead")
 MSG_DEF(JSMSG_DUPLICATE_FORMAL,        1, JSEXN_SYNTAXERR, "duplicate formal argument {0}")
 MSG_DEF(JSMSG_DUPLICATE_LABEL,         0, JSEXN_SYNTAXERR, "duplicate label")
 MSG_DEF(JSMSG_DUPLICATE_PROPERTY,      1, JSEXN_SYNTAXERR, "property name {0} appears more than once in object literal")
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -41,16 +41,17 @@ JSCompartment::JSCompartment(Zone *zone,
   : options_(options),
     zone_(zone),
     runtime_(zone->runtimeFromMainThread()),
     principals(nullptr),
     isSystem(false),
     isSelfHosting(false),
     marked(true),
     warnedAboutNoSuchMethod(false),
+    warnedAboutFlagsArgument(false),
     addonId(options.addonIdOrNull()),
 #ifdef DEBUG
     firedOnNewGlobalObject(false),
 #endif
     global_(nullptr),
     enterCompartmentDepth(0),
     totalTime(0),
     data(nullptr),
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -144,16 +144,17 @@ struct JSCompartment
     JSRuntime                    *runtime_;
 
   public:
     JSPrincipals                 *principals;
     bool                         isSystem;
     bool                         isSelfHosting;
     bool                         marked;
     bool                         warnedAboutNoSuchMethod;
+    bool                         warnedAboutFlagsArgument;
 
     // A null add-on ID means that the compartment is not associated with an
     // add-on.
     JSAddonId                    *addonId;
 
 #ifdef DEBUG
     bool                         firedOnNewGlobalObject;
 #endif
@@ -542,16 +543,17 @@ struct JSCompartment
     enum DeprecatedLanguageExtension {
         DeprecatedForEach = 0,              // JS 1.6+
         DeprecatedDestructuringForIn = 1,   // JS 1.7 only
         DeprecatedLegacyGenerator = 2,      // JS 1.7+
         DeprecatedExpressionClosure = 3,    // Added in JS 1.8
         DeprecatedLetBlock = 4,             // Added in JS 1.7
         DeprecatedLetExpression = 5,        // Added in JS 1.7
         DeprecatedNoSuchMethod = 6,         // JS 1.7+
+        DeprecatedFlagsArgument = 7,        // JS 1.3 or older
         DeprecatedLanguageExtensionCount
     };
 
   private:
     // Used for collecting telemetry on SpiderMonkey's deprecated language extensions.
     bool sawDeprecatedLanguageExtension[DeprecatedLanguageExtensionCount];
 
     void reportTelemetry();
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -1765,17 +1765,17 @@ GCMarker::delayMarkingChildren(const voi
     const TenuredCell *cell = TenuredCell::fromPointer(thing);
     cell->arenaHeader()->markOverflow = 1;
     delayMarkingArena(cell->arenaHeader());
 }
 
 inline void
 ArenaLists::prepareForIncrementalGC(JSRuntime *rt)
 {
-    for (ALL_ALLOC_KINDS(i)) {
+    for (auto i : AllAllocKinds()) {
         FreeList *freeList = &freeLists[i];
         if (!freeList->isEmpty()) {
             ArenaHeader *aheader = freeList->arenaHeader();
             aheader->allocatedDuringIncremental = true;
             rt->gc.marker.delayMarkingArena(aheader);
         }
     }
 }
@@ -2083,39 +2083,39 @@ ArenaLists::relocateArenas(ArenaHeader *
     MOZ_ASSERT(runtime_->isHeapCompacting());
     MOZ_ASSERT(!runtime_->gc.isBackgroundSweeping());
 
     // Flush all the freeLists back into the arena headers
     purge();
     checkEmptyFreeLists();
 
     if (ShouldRelocateAllArenas(reason)) {
-        for (ALL_ALLOC_KINDS(i)) {
+        for (auto i : AllAllocKinds()) {
             if (CanRelocateAllocKind(i)) {
                 ArenaList &al = arenaLists[i];
                 ArenaHeader *allArenas = al.head();
                 al.clear();
                 relocatedListOut = al.relocateArenas(allArenas, relocatedListOut, sliceBudget, stats);
             }
         }
     } else {
         size_t arenaCount = 0;
         size_t relocCount = 0;
         AllAllocKindArray<ArenaHeader **> toRelocate;
 
-        for (ALL_ALLOC_KINDS(i)) {
+        for (auto i : AllAllocKinds()) {
             toRelocate[i] = nullptr;
             if (CanRelocateAllocKind(i))
                 toRelocate[i] = arenaLists[i].pickArenasToRelocate(arenaCount, relocCount);
         }
 
         if (!ShouldRelocateZone(arenaCount, relocCount, reason))
             return false;
 
-        for (ALL_ALLOC_KINDS(i)) {
+        for (auto i : AllAllocKinds()) {
             if (toRelocate[i]) {
                 ArenaList &al = arenaLists[i];
                 ArenaHeader *arenas = al.removeRemainingArenas(toRelocate[i]);
                 relocatedListOut = al.relocateArenas(arenas, relocatedListOut, sliceBudget, stats);
             }
         }
     }
 
@@ -2140,17 +2140,17 @@ GCRuntime::relocateArenas(Zone *zone, JS
     jit::StopAllOffThreadCompilations(zone);
 
     if (!zone->arenas.relocateArenas(relocatedArenasToRelease, reason, sliceBudget, stats))
         return false;
 
 #ifdef DEBUG
     // Check that we did as much compaction as we should have. There
     // should always be less than one arena's worth of free cells.
-    for (ALL_ALLOC_KINDS(i)) {
+    for (auto i : AllAllocKinds()) {
         size_t thingsPerArena = Arena::thingsPerArena(Arena::thingSize(i));
         if (CanRelocateAllocKind(i)) {
             ArenaList &al = zone->arenas.arenaLists[i];
             size_t freeCells = 0;
             for (ArenaHeader *arena = al.arenaAfterCursor(); arena; arena = arena->next)
                 freeCells += arena->countFreeCells();
             MOZ_ASSERT(freeCells < thingsPerArena);
         }
@@ -2683,27 +2683,27 @@ ReleaseArenaList(JSRuntime *rt, ArenaHea
         rt->gc.releaseArena(aheader, lock);
     }
 }
 
 ArenaLists::~ArenaLists()
 {
     AutoLockGC lock(runtime_);
 
-    for (ALL_ALLOC_KINDS(i)) {
+    for (auto i : AllAllocKinds()) {
         /*
          * We can only call this during the shutdown after the last GC when
          * the background finalization is disabled.
          */
         MOZ_ASSERT(backgroundFinalizeState[i] == BFS_DONE);
         ReleaseArenaList(runtime_, arenaLists[i].head(), lock);
     }
     ReleaseArenaList(runtime_, incrementalSweptArenas.head(), lock);
 
-    for (OBJECT_ALLOC_KINDS(i))
+    for (auto i : ObjectAllocKinds())
         ReleaseArenaList(runtime_, savedObjectArenas[i].head(), lock);
     ReleaseArenaList(runtime_, savedEmptyObjectArenas, lock);
 }
 
 void
 ArenaLists::finalizeNow(FreeOp *fop, const FinalizePhase& phase)
 {
     gcstats::AutoPhase ap(fop->runtime()->gc.stats, phase.statsPhase);
@@ -2835,18 +2835,19 @@ ArenaLists::backgroundFinalize(FreeOp *f
 }
 
 void
 ArenaLists::queueForegroundObjectsForSweep(FreeOp *fop)
 {
     gcstats::AutoPhase ap(fop->runtime()->gc.stats, gcstats::PHASE_SWEEP_OBJECT);
 
 #ifdef DEBUG
-    for (OBJECT_ALLOC_KINDS(i))
+    for (auto i : ObjectAllocKinds()) { // Braces needed to appease MSVC 2013.
         MOZ_ASSERT(savedObjectArenas[i].isEmpty());
+    }
     MOZ_ASSERT(savedEmptyObjectArenas == nullptr);
 #endif
 
     // Foreground finalized objects must be finalized at the beginning of the
     // sweep phase, before control can return to the mutator. Otherwise,
     // mutator behavior can resurrect certain objects whose references would
     // otherwise have been erased by the finalizer.
     finalizeNow(fop, AllocKind::OBJECT0, KEEP_ARENAS, &savedEmptyObjectArenas);
@@ -3229,17 +3230,17 @@ GCRuntime::sweepBackgroundThings(ZoneLis
 }
 
 void
 GCRuntime::assertBackgroundSweepingFinished()
 {
 #ifdef DEBUG
     MOZ_ASSERT(backgroundSweepZones.isEmpty());
     for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
-        for (ALL_ALLOC_KINDS(i)) {
+        for (auto i : AllAllocKinds()) {
             MOZ_ASSERT(!zone->arenas.arenaListsToSweep[i]);
             MOZ_ASSERT(zone->arenas.doneBackgroundFinalize(i));
         }
     }
     MOZ_ASSERT(freeLifoAlloc.computedSizeOfExcludingThis() == 0);
 #endif
 }
 
@@ -3701,17 +3702,17 @@ void
 GCRuntime::checkForCompartmentMismatches()
 {
     if (disableStrictProxyCheckingCount)
         return;
 
     CompartmentCheckTracer trc(rt, CheckCompartmentCallback);
     for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) {
         trc.zone = zone;
-        for (ALL_ALLOC_KINDS(thingKind)) {
+        for (auto thingKind : AllAllocKinds()) {
             for (ZoneCellIterUnderGC i(zone, thingKind); !i.done(); i.next()) {
                 trc.src = i.getCell();
                 trc.srcKind = MapAllocToTraceKind(thingKind);
                 trc.compartment = CompartmentOfCell(trc.src, trc.srcKind);
                 JS_TraceChildren(&trc, trc.src, trc.srcKind);
             }
         }
     }
@@ -3730,18 +3731,21 @@ GCRuntime::beginMarkPhase(JS::gcreason::
 
     isFull = true;
     bool any = false;
 
     for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
         /* Assert that zone state is as we expect */
         MOZ_ASSERT(!zone->isCollecting());
         MOZ_ASSERT(!zone->compartments.empty());
-        for (ALL_ALLOC_KINDS(i))
+#ifdef DEBUG
+        for (auto i : AllAllocKinds()) { // Braces needed to appease MSVC 2013.
             MOZ_ASSERT(!zone->arenas.arenaListsToSweep[i]);
+        }
+#endif
 
         /* Set up which zones will be collected. */
         if (zone->isGCScheduled()) {
             if (!rt->isAtomsZone(zone)) {
                 any = true;
                 zone->setGCState(Zone::Mark);
             }
         } else {
@@ -5290,17 +5294,17 @@ GCRuntime::endSweepPhase(bool destroying
         if (destroyingRuntime)
             sweepZones(&fop, destroyingRuntime);
     }
 
     finishMarkingValidation();
 
 #ifdef DEBUG
     for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
-        for (ALL_ALLOC_KINDS(i)) {
+        for (auto i : AllAllocKinds()) {
             MOZ_ASSERT_IF(!IsBackgroundFinalized(i) ||
                           !sweepOnBackgroundThread,
                           !zone->arenas.arenaListsToSweep[i]);
         }
     }
 
     for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) {
         MOZ_ASSERT(!c->gcIncomingGrayPointers);
@@ -6465,17 +6469,17 @@ gc::MergeCompartments(JSCompartment *sou
     for (ZoneCellIter iter(source->zone(), AllocKind::OBJECT_GROUP); !iter.done(); iter.next()) {
         ObjectGroup *group = iter.get<ObjectGroup>();
         group->setGeneration(target->zone()->types.generation);
         group->compartment_ = target;
     }
 
     // Fixup zone pointers in source's zone to refer to target's zone.
 
-    for (ALL_ALLOC_KINDS(thingKind)) {
+    for (auto thingKind : AllAllocKinds()) {
         for (ArenaIter aiter(source->zone(), thingKind); !aiter.done(); aiter.next()) {
             ArenaHeader *aheader = aiter.get();
             aheader->zone = target->zone();
         }
     }
 
     // The source should be the only compartment in its zone.
     for (CompartmentsInZoneIter c(source->zone()); !c.done(); c.next())
@@ -6661,17 +6665,17 @@ ArenaLists::normalizeBackgroundFinalizeS
 void
 ArenaLists::adoptArenas(JSRuntime *rt, ArenaLists *fromArenaLists)
 {
     // GC should be inactive, but still take the lock as a kind of read fence.
     AutoLockGC lock(rt);
 
     fromArenaLists->purge();
 
-    for (ALL_ALLOC_KINDS(thingKind)) {
+    for (auto thingKind : AllAllocKinds()) {
         // When we enter a parallel section, we join the background
         // thread, and we do not run GC while in the parallel section,
         // so no finalizer should be active!
         normalizeBackgroundFinalizeState(thingKind);
         fromArenaLists->normalizeBackgroundFinalizeState(thingKind);
 
         ArenaList *fromList = &fromArenaLists->arenaLists[thingKind];
         ArenaList *toList = &arenaLists[thingKind];
--- a/js/src/jsgc.h
+++ b/js/src/jsgc.h
@@ -613,21 +613,21 @@ class ArenaLists
     // objects which have already been finalized in the foreground (which must
     // happen at the beginning of the GC), so that type sweeping can determine
     // which of the object pointers are marked.
     ObjectAllocKindArray<ArenaList> savedObjectArenas;
     ArenaHeader *savedEmptyObjectArenas;
 
   public:
     explicit ArenaLists(JSRuntime *rt) : runtime_(rt) {
-        for (ALL_ALLOC_KINDS(i))
+        for (auto i : AllAllocKinds())
             freeLists[i].initAsEmpty();
-        for (ALL_ALLOC_KINDS(i))
+        for (auto i : AllAllocKinds())
             backgroundFinalizeState[i] = BFS_DONE;
-        for (ALL_ALLOC_KINDS(i))
+        for (auto i : AllAllocKinds())
             arenaListsToSweep[i] = nullptr;
         incrementalSweptArenaKind = AllocKind::LIMIT;
         gcShapeArenasToUpdate = nullptr;
         gcAccessorShapeArenasToUpdate = nullptr;
         gcScriptArenasToUpdate = nullptr;
         gcObjectGroupArenasToUpdate = nullptr;
         savedEmptyObjectArenas = nullptr;
     }
@@ -657,31 +657,31 @@ class ArenaLists
         return incrementalSweptArenas.head();
     }
 
     ArenaHeader *getArenaAfterCursor(AllocKind thingKind) const {
         return arenaLists[thingKind].arenaAfterCursor();
     }
 
     bool arenaListsAreEmpty() const {
-        for (ALL_ALLOC_KINDS(i)) {
+        for (auto i : AllAllocKinds()) {
             /*
              * The arena cannot be empty if the background finalization is not yet
              * done.
              */
             if (backgroundFinalizeState[i] != BFS_DONE)
                 return false;
             if (!arenaLists[i].isEmpty())
                 return false;
         }
         return true;
     }
 
     void unmarkAll() {
-        for (ALL_ALLOC_KINDS(i)) {
+        for (auto i : AllAllocKinds()) {
             /* The background finalization must have stopped at this point. */
             MOZ_ASSERT(backgroundFinalizeState[i] == BFS_DONE);
             for (ArenaHeader *aheader = arenaLists[i].head(); aheader; aheader = aheader->next)
                 aheader->unmarkAll();
         }
     }
 
     bool doneBackgroundFinalize(AllocKind kind) const {
@@ -692,17 +692,17 @@ class ArenaLists
         return backgroundFinalizeState[kind] != BFS_DONE;
     }
 
     /*
      * Return the free list back to the arena so the GC finalization will not
      * run the finalizers over unitialized bytes from free things.
      */
     void purge() {
-        for (ALL_ALLOC_KINDS(i))
+        for (auto i : AllAllocKinds())
             purge(i);
     }
 
     void purge(AllocKind i) {
         FreeList *freeList = &freeLists[i];
         if (!freeList->isEmpty()) {
             ArenaHeader *aheader = freeList->arenaHeader();
             aheader->setFirstFreeSpan(freeList->getHead());
@@ -713,17 +713,17 @@ class ArenaLists
     inline void prepareForIncrementalGC(JSRuntime *rt);
 
     /*
      * Temporarily copy the free list heads to the arenas so the code can see
      * the proper value in ArenaHeader::freeList when accessing the latter
      * outside the GC.
      */
     void copyFreeListsToArenas() {
-        for (ALL_ALLOC_KINDS(i))
+        for (auto i : AllAllocKinds())
             copyFreeListToArena(i);
     }
 
     void copyFreeListToArena(AllocKind thingKind) {
         FreeList *freeList = &freeLists[thingKind];
         if (!freeList->isEmpty()) {
             ArenaHeader *aheader = freeList->arenaHeader();
             MOZ_ASSERT(!aheader->hasFreeThings());
@@ -731,17 +731,17 @@ class ArenaLists
         }
     }
 
     /*
      * Clear the free lists in arenas that were temporarily set there using
      * copyToArenas.
      */
     void clearFreeListsInArenas() {
-        for (ALL_ALLOC_KINDS(i))
+        for (auto i : AllAllocKinds())
             clearFreeListInArena(i);
     }
 
     void clearFreeListInArena(AllocKind kind) {
         FreeList *freeList = &freeLists[kind];
         if (!freeList->isEmpty()) {
             ArenaHeader *aheader = freeList->arenaHeader();
             MOZ_ASSERT(freeList->isSameNonEmptySpan(aheader->getFirstFreeSpan()));
@@ -787,17 +787,17 @@ class ArenaLists
      */
     void adoptArenas(JSRuntime *runtime, ArenaLists *fromArenaLists);
 
     /* True if the ArenaHeader in question is found in this ArenaLists */
     bool containsArena(JSRuntime *runtime, ArenaHeader *arenaHeader);
 
     void checkEmptyFreeLists() {
 #ifdef DEBUG
-        for (ALL_ALLOC_KINDS(i))
+        for (auto i : AllAllocKinds())
             checkEmptyFreeList(i);
 #endif
     }
 
     void checkEmptyFreeList(AllocKind kind) {
         MOZ_ASSERT(freeLists[kind].isEmpty());
     }
 
--- a/js/src/jsopcode.cpp
+++ b/js/src/jsopcode.cpp
@@ -293,17 +293,17 @@ js::DumpCompartmentPCCounts(JSContext *c
 
             fprintf(stdout, "--- SCRIPT %s:%" PRIuSIZE " ---\n", script->filename(), script->lineno());
             DumpPCCounts(cx, script, &sprinter);
             fputs(sprinter.string(), stdout);
             fprintf(stdout, "--- END SCRIPT %s:%" PRIuSIZE " ---\n", script->filename(), script->lineno());
         }
     }
 
-    for (OBJECT_ALLOC_KINDS(thingKind)) {
+    for (auto thingKind : ObjectAllocKinds()) {
         for (ZoneCellIter i(cx->zone(), thingKind); !i.done(); i.next()) {
             JSObject *obj = i.get<JSObject>();
             if (obj->compartment() != cx->compartment())
                 continue;
 
             if (obj->is<AsmJSModuleObject>()) {
                 AsmJSModule &module = obj->as<AsmJSModuleObject>().module();
 
--- a/js/src/jsstr.cpp
+++ b/js/src/jsstr.cpp
@@ -2127,16 +2127,28 @@ class MOZ_STACK_CLASS StringRegExpGuard
     bool normalizeRegExp(JSContext *cx, bool flat, unsigned optarg, CallArgs args)
     {
         if (re_.initialized())
             return true;
 
         /* Build RegExp from pattern string. */
         RootedString opt(cx);
         if (optarg < args.length()) {
+            if (JSScript *script = cx->currentScript()) {
+                const char *filename = script->filename();
+                cx->compartment()->addTelemetry(filename, JSCompartment::DeprecatedFlagsArgument);
+            }
+
+            if (!cx->compartment()->warnedAboutFlagsArgument) {
+                if (!JS_ReportErrorFlagsAndNumber(cx, JSREPORT_WARNING, GetErrorMessage, nullptr,
+                                                  JSMSG_DEPRECATED_FLAGS_ARG))
+                    return false;
+                cx->compartment()->warnedAboutFlagsArgument = true;
+            }
+
             opt = ToString<CanGC>(cx, args[optarg]);
             if (!opt)
                 return false;
         } else {
             opt = nullptr;
         }
 
         Rooted<JSAtom *> pat(cx);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/js1_5/String/replace-flags.js
@@ -0,0 +1,15 @@
+// |reftest| skip-if(!xulRuntime.shell)
+
+var BUGNUMBER = 1142351;
+var summary = 'Add console warnings for non-standard flag argument of String.prototype.{search,match,replace}.';
+
+printBugNumber(BUGNUMBER);
+printStatus (summary);
+
+options("werror");
+assertEq(evaluate("'aaaA'.match('a', 'i')", {catchTermination: true}), "terminated");
+assertEq(evaluate("'aaaA'.search('a', 'i')", {catchTermination: true}), "terminated");
+assertEq(evaluate("'aaaA'.replace('a', 'b', 'g')", {catchTermination: true}), "terminated");
+
+if (typeof reportCompare === "function")
+  reportCompare(true, true);
--- a/js/src/vm/Xdr.h
+++ b/js/src/vm/Xdr.h
@@ -24,21 +24,21 @@ namespace js {
  * versions.  If deserialization fails, the data should be invalidated if
  * possible.
  *
  * When you change this, run make_opcode_doc.py and copy the new output into
  * this wiki page:
  *
  *  https://developer.mozilla.org/en-US/docs/SpiderMonkey/Internals/Bytecode
  */
-static const uint32_t XDR_BYTECODE_VERSION_SUBTRAHEND = 267;
+static const uint32_t XDR_BYTECODE_VERSION_SUBTRAHEND = 268;
 static const uint32_t XDR_BYTECODE_VERSION =
     uint32_t(0xb973c0de - XDR_BYTECODE_VERSION_SUBTRAHEND);
 
-static_assert(JSErr_Limit == 388,
+static_assert(JSErr_Limit == 389,
               "GREETINGS, POTENTIAL SUBTRAHEND INCREMENTER! If you added or "
               "removed MSG_DEFs from js.msg, you should increment "
               "XDR_BYTECODE_VERSION_SUBTRAHEND and update this assertion's "
               "expected JSErr_Limit value.");
 
 class XDRBuffer {
   public:
     explicit XDRBuffer(JSContext *cx)
--- a/layout/base/FrameLayerBuilder.cpp
+++ b/layout/base/FrameLayerBuilder.cpp
@@ -380,16 +380,19 @@ public:
    */
   void AccumulateEventRegions(nsDisplayLayerEventRegions* aEventRegions)
   {
     FLB_LOG_PAINTED_LAYER_DECISION(this, "Accumulating event regions %p against pld=%p\n", aEventRegions, this);
 
     mHitRegion.Or(mHitRegion, aEventRegions->HitRegion());
     mMaybeHitRegion.Or(mMaybeHitRegion, aEventRegions->MaybeHitRegion());
     mDispatchToContentHitRegion.Or(mDispatchToContentHitRegion, aEventRegions->DispatchToContentHitRegion());
+    mNoActionRegion.Or(mNoActionRegion, aEventRegions->NoActionRegion());
+    mHorizontalPanRegion.Or(mHorizontalPanRegion, aEventRegions->HorizontalPanRegion());
+    mVerticalPanRegion.Or(mVerticalPanRegion, aEventRegions->VerticalPanRegion());
   }
 
   /**
    * If this represents only a nsDisplayImage, and the image type
    * supports being optimized to an ImageLayer (TYPE_RASTER only) returns
    * an ImageContainer for the image.
    */
   already_AddRefed<ImageContainer> CanOptimizeImageLayer(nsDisplayListBuilder* aBuilder);
@@ -429,16 +432,37 @@ public:
    * The maybe-hit region for this PaintedLayer.
    */
   nsRegion  mMaybeHitRegion;
   /**
    * The dispatch-to-content hit region for this PaintedLayer.
    */
   nsRegion  mDispatchToContentHitRegion;
   /**
+   * The region for this PaintedLayer that is sensitive to events
+   * but disallows panning and zooming. This is an approximation
+   * and any deviation from the true region will be part of the
+   * mDispatchToContentHitRegion.
+   */
+  nsRegion mNoActionRegion;
+  /**
+   * The region for this PaintedLayer that is sensitive to events and
+   * allows horizontal panning but not zooming. This is an approximation
+   * and any deviation from the true region will be part of the
+   * mDispatchToContentHitRegion.
+   */
+  nsRegion mHorizontalPanRegion;
+  /**
+   * The region for this PaintedLayer that is sensitive to events and
+   * allows vertical panning but not zooming. This is an approximation
+   * and any deviation from the true region will be part of the
+   * mDispatchToContentHitRegion.
+   */
+  nsRegion mVerticalPanRegion;
+  /**
    * The "active scrolled root" for all content in the layer. Must
    * be non-null; all content in a PaintedLayer must have the same
    * active scrolled root.
    */
   const nsIFrame* mAnimatedGeometryRoot;
   /**
    * The offset between mAnimatedGeometryRoot and the reference frame.
    */
@@ -1958,29 +1982,31 @@ SetOuterVisibleRegion(Layer* aLayer, nsI
       aOuterVisibleRegion->And(*aOuterVisibleRegion, *aLayerContentsVisibleRect);
     }
   } else {
     nsIntRect outerRect = aOuterVisibleRegion->GetBounds();
     // if 'transform' is not invertible, then nothing will be displayed
     // for the layer, so it doesn't really matter what we do here
     Rect outerVisible(outerRect.x, outerRect.y, outerRect.width, outerRect.height);
     transform.Invert();
-    gfxRect layerVisible = ThebesRect(transform.ProjectRectBounds(outerVisible));
+
+    Rect layerContentsVisible(-float(INT32_MAX) / 2, -float(INT32_MAX) / 2,
+                              float(INT32_MAX), float(INT32_MAX));
     if (aLayerContentsVisibleRect) {
       NS_ASSERTION(aLayerContentsVisibleRect->width >= 0 &&
                    aLayerContentsVisibleRect->height >= 0,
                    "Bad layer contents rectangle");
       // restrict to aLayerContentsVisibleRect before call GfxRectToIntRect,
       // in case layerVisible is extremely large (as it can be when
       // projecting through the inverse of a 3D transform)
-      gfxRect layerContentsVisible(
+      layerContentsVisible = Rect(
           aLayerContentsVisibleRect->x, aLayerContentsVisibleRect->y,
           aLayerContentsVisibleRect->width, aLayerContentsVisibleRect->height);
-      layerVisible.IntersectRect(layerVisible, layerContentsVisible);
     }
+    gfxRect layerVisible = ThebesRect(transform.ProjectRectBounds(outerVisible, layerContentsVisible));
     layerVisible.RoundOut();
     nsIntRect visRect;
     if (gfxUtils::GfxRectToIntRect(layerVisible, &visRect)) {
       *aOuterVisibleRegion = visRect;
     } else  {
       aOuterVisibleRegion->SetEmpty();
     }
   }
@@ -2404,47 +2430,64 @@ ContainerState::PopPaintedLayerData()
     if (!data->mMaybeHitRegion.GetBounds().IsEmpty()) {
       nsRect rect = nsLayoutUtils::TransformFrameRectToAncestor(
         mContainerReferenceFrame,
         data->mMaybeHitRegion.GetBounds(),
         containingPaintedLayerData->mReferenceFrame);
       containingPaintedLayerData->mMaybeHitRegion.Or(
         containingPaintedLayerData->mMaybeHitRegion, rect);
     }
-    if (!data->mHitRegion.GetBounds().IsEmpty()) {
-      // Our definitely-hit region must go to the maybe-hit-region since
-      // this function is an approximation.
-      Matrix4x4 matrix = nsLayoutUtils::GetTransformToAncestor(
-        mContainerReferenceFrame, containingPaintedLayerData->mReferenceFrame);
-      Matrix matrix2D;
-      bool isPrecise = matrix.Is2D(&matrix2D) && !matrix2D.HasNonAxisAlignedTransform();
-      nsRect rect = nsLayoutUtils::TransformFrameRectToAncestor(
-        mContainerReferenceFrame,
-        data->mHitRegion.GetBounds(),
-        containingPaintedLayerData->mReferenceFrame);
-      nsRegion* dest = isPrecise ? &containingPaintedLayerData->mHitRegion
-                                 : &containingPaintedLayerData->mMaybeHitRegion;
-      dest->Or(*dest, rect);
-    }
+    nsLayoutUtils::TransformToAncestorAndCombineRegions(
+      data->mHitRegion.GetBounds(),
+      mContainerReferenceFrame,
+      containingPaintedLayerData->mReferenceFrame,
+      &containingPaintedLayerData->mHitRegion,
+      &containingPaintedLayerData->mMaybeHitRegion);
+    nsLayoutUtils::TransformToAncestorAndCombineRegions(
+      data->mNoActionRegion.GetBounds(),
+      mContainerReferenceFrame,
+      containingPaintedLayerData->mReferenceFrame,
+      &containingPaintedLayerData->mNoActionRegion,
+      &containingPaintedLayerData->mDispatchToContentHitRegion);
+    nsLayoutUtils::TransformToAncestorAndCombineRegions(
+      data->mHorizontalPanRegion.GetBounds(),
+      mContainerReferenceFrame,
+      containingPaintedLayerData->mReferenceFrame,
+      &containingPaintedLayerData->mHorizontalPanRegion,
+      &containingPaintedLayerData->mDispatchToContentHitRegion);
+    nsLayoutUtils::TransformToAncestorAndCombineRegions(
+      data->mVerticalPanRegion.GetBounds(),
+      mContainerReferenceFrame,
+      containingPaintedLayerData->mReferenceFrame,
+      &containingPaintedLayerData->mVerticalPanRegion,
+      &containingPaintedLayerData->mDispatchToContentHitRegion);
+
   } else {
     EventRegions regions;
     regions.mHitRegion = ScaleRegionToOutsidePixels(data->mHitRegion);
+    regions.mNoActionRegion = ScaleRegionToOutsidePixels(data->mNoActionRegion);
+    regions.mHorizontalPanRegion = ScaleRegionToOutsidePixels(data->mHorizontalPanRegion);
+    regions.mVerticalPanRegion = ScaleRegionToOutsidePixels(data->mVerticalPanRegion);
+
     // Points whose hit-region status we're not sure about need to be dispatched
     // to the content thread. If a point is in both maybeHitRegion and hitRegion
     // then it's not a "maybe" any more, and doesn't go into the dispatch-to-
     // content region.
     nsIntRegion maybeHitRegion = ScaleRegionToOutsidePixels(data->mMaybeHitRegion);
     regions.mDispatchToContentHitRegion.Sub(maybeHitRegion, regions.mHitRegion);
     regions.mDispatchToContentHitRegion.OrWith(
         ScaleRegionToOutsidePixels(data->mDispatchToContentHitRegion));
     regions.mHitRegion.OrWith(maybeHitRegion);
 
     nsIntPoint translation = -GetTranslationForPaintedLayer(data->mLayer);
     regions.mHitRegion.MoveBy(translation);
     regions.mDispatchToContentHitRegion.MoveBy(translation);
+    regions.mNoActionRegion.MoveBy(translation);
+    regions.mHorizontalPanRegion.MoveBy(translation);
+    regions.mVerticalPanRegion.MoveBy(translation);
 
     layer->SetEventRegions(regions);
   }
 
   // Since we're going to pop off the last PaintedLayerData, the
   // mVisibleAboveRegion of the second-to-last item will need to include
   // the regions of the last item. If we're emptying the PaintedLayerDataStack,
   // we instead need to accumulate the regions into the container's
--- a/layout/base/nsDisplayList.cpp
+++ b/layout/base/nsDisplayList.cpp
@@ -3224,16 +3224,30 @@ nsDisplayLayerEventRegions::AddFrame(nsD
       (aFrame->GetStateBits() & NS_FRAME_SVG_LAYOUT)) {
     mMaybeHitRegion.Or(mMaybeHitRegion, borderBox);
   } else {
     mHitRegion.Or(mHitRegion, borderBox);
   }
   if (aBuilder->GetAncestorHasApzAwareEventHandler()) {
     mDispatchToContentHitRegion.Or(mDispatchToContentHitRegion, borderBox);
   }
+
+  // Touch action region
+
+  uint32_t touchAction = nsLayoutUtils::GetTouchActionFromFrame(aFrame);
+  if (touchAction & NS_STYLE_TOUCH_ACTION_NONE) {
+    mNoActionRegion.Or(mNoActionRegion, borderBox);
+  } else {
+    if ((touchAction & NS_STYLE_TOUCH_ACTION_PAN_X)) {
+      mHorizontalPanRegion.Or(mHorizontalPanRegion, borderBox);
+    }
+    if ((touchAction & NS_STYLE_TOUCH_ACTION_PAN_Y)) {
+      mVerticalPanRegion.Or(mVerticalPanRegion, borderBox);
+    }
+  }
 }
 
 void
 nsDisplayLayerEventRegions::AddInactiveScrollPort(const nsRect& aRect)
 {
   mDispatchToContentHitRegion.Or(mDispatchToContentHitRegion, aRect);
 }
 
@@ -5682,25 +5696,25 @@ void nsDisplayTransform::HitTest(nsDispl
                            1, 1);
 
   } else {
     Rect originalRect(NSAppUnitsToFloatPixels(aRect.x, factor),
                       NSAppUnitsToFloatPixels(aRect.y, factor),
                       NSAppUnitsToFloatPixels(aRect.width, factor),
                       NSAppUnitsToFloatPixels(aRect.height, factor));
 
-    Rect rect = matrix.ProjectRectBounds(originalRect);
 
     bool snap;
     nsRect childBounds = mStoredList.GetBounds(aBuilder, &snap);
     Rect childGfxBounds(NSAppUnitsToFloatPixels(childBounds.x, factor),
                         NSAppUnitsToFloatPixels(childBounds.y, factor),
                         NSAppUnitsToFloatPixels(childBounds.width, factor),
                         NSAppUnitsToFloatPixels(childBounds.height, factor));
-    rect = rect.Intersect(childGfxBounds);
+
+    Rect rect = matrix.ProjectRectBounds(originalRect, childGfxBounds);
 
     resultingRect = nsRect(NSFloatPixelsToAppUnits(float(rect.X()), factor),
                            NSFloatPixelsToAppUnits(float(rect.Y()), factor),
                            NSFloatPixelsToAppUnits(float(rect.Width()), factor),
                            NSFloatPixelsToAppUnits(float(rect.Height()), factor));
   }
 
   if (resultingRect.IsEmpty()) {
@@ -5930,18 +5944,17 @@ bool nsDisplayTransform::UntransformRect
               NSAppUnitsToFloatPixels(aTransformedBounds.width, factor),
               NSAppUnitsToFloatPixels(aTransformedBounds.height, factor));
 
   Rect childGfxBounds(NSAppUnitsToFloatPixels(aChildBounds.x, factor),
                       NSAppUnitsToFloatPixels(aChildBounds.y, factor),
                       NSAppUnitsToFloatPixels(aChildBounds.width, factor),
                       NSAppUnitsToFloatPixels(aChildBounds.height, factor));
 
-  result = ToMatrix4x4(transform.Inverse()).ProjectRectBounds(result);
-  result = result.Intersect(childGfxBounds);
+  result = ToMatrix4x4(transform.Inverse()).ProjectRectBounds(result, childGfxBounds);
   *aOutRect = nsLayoutUtils::RoundGfxRectToAppRect(ThebesRect(result), factor);
   return true;
 }
 
 bool nsDisplayTransform::UntransformVisibleRect(nsDisplayListBuilder* aBuilder,
                                                 nsRect *aOutRect)
 {
   const gfx3DMatrix& matrix = To3DMatrix(GetTransform());
@@ -5958,18 +5971,17 @@ bool nsDisplayTransform::UntransformVisi
   bool snap;
   nsRect childBounds = mStoredList.GetBounds(aBuilder, &snap);
   Rect childGfxBounds(NSAppUnitsToFloatPixels(childBounds.x, factor),
                       NSAppUnitsToFloatPixels(childBounds.y, factor),
                       NSAppUnitsToFloatPixels(childBounds.width, factor),
                       NSAppUnitsToFloatPixels(childBounds.height, factor));
 
   /* We want to untransform the matrix, so invert the transformation first! */
-  result = ToMatrix4x4(matrix.Inverse()).ProjectRectBounds(result);
-  result = result.Intersect(childGfxBounds);
+  result = ToMatrix4x4(matrix.Inverse()).ProjectRectBounds(result, childGfxBounds);
 
   *aOutRect = nsLayoutUtils::RoundGfxRectToAppRect(ThebesRect(result), factor);
 
   return true;
 }
 
 void
 nsDisplayTransform::WriteDebugInfo(std::stringstream& aStream)
--- a/layout/base/nsDisplayList.h
+++ b/layout/base/nsDisplayList.h
@@ -2706,29 +2706,41 @@ public:
   // Indicate that an inactive scrollframe's scrollport should be added to the
   // dispatch-to-content region, to ensure that APZ lets content create a
   // displayport.
   void AddInactiveScrollPort(const nsRect& aRect);
 
   const nsRegion& HitRegion() { return mHitRegion; }
   const nsRegion& MaybeHitRegion() { return mMaybeHitRegion; }
   const nsRegion& DispatchToContentHitRegion() { return mDispatchToContentHitRegion; }
+  const nsRegion& NoActionRegion() { return mNoActionRegion; }
+  const nsRegion& HorizontalPanRegion() { return mHorizontalPanRegion; }
+  const nsRegion& VerticalPanRegion() { return mVerticalPanRegion; }
 
   virtual void WriteDebugInfo(std::stringstream& aStream) override;
 
 private:
   // Relative to aFrame's reference frame.
   // These are the points that are definitely in the hit region.
   nsRegion mHitRegion;
   // These are points that may or may not be in the hit region. Only main-thread
   // event handling can tell for sure (e.g. because complex shapes are present).
   nsRegion mMaybeHitRegion;
   // These are points that need to be dispatched to the content thread for
   // resolution. Always contained in the union of mHitRegion and mMaybeHitRegion.
   nsRegion mDispatchToContentHitRegion;
+  // These are points where panning is disabled, as determined by the touch-action
+  // property. Always contained in the union of mHitRegion and mMaybeHitRegion.
+  nsRegion mNoActionRegion;
+  // These are points where panning is horizontal, as determined by the touch-action
+  // property. Always contained in the union of mHitRegion and mMaybeHitRegion.
+  nsRegion mHorizontalPanRegion;
+  // These are points where panning is vertical, as determined by the touch-action
+  // property. Always contained in the union of mHitRegion and mMaybeHitRegion.
+  nsRegion mVerticalPanRegion;
 };
 
 /**
  * A class that lets you wrap a display list as a display item.
  * 
  * GetUnderlyingFrame() is troublesome for wrapped lists because if the wrapped
  * list has many items, it's not clear which one has the 'underlying frame'.
  * Thus we force the creator to specify what the underlying frame is. The
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -2499,17 +2499,25 @@ nsLayoutUtils::TransformRect(nsIFrame* a
     1.0f / aFromFrame->PresContext()->AppUnitsPerDevPixel();
   float devPixelsPerAppUnitToFrame =
     1.0f / aToFrame->PresContext()->AppUnitsPerDevPixel();
   gfx::Rect toDevPixels = downToDest.ProjectRectBounds(
     upToAncestor.ProjectRectBounds(
       gfx::Rect(aRect.x * devPixelsPerAppUnitFromFrame,
                 aRect.y * devPixelsPerAppUnitFromFrame,
                 aRect.width * devPixelsPerAppUnitFromFrame,
-                aRect.height * devPixelsPerAppUnitFromFrame)));
+                aRect.height * devPixelsPerAppUnitFromFrame),
+      Rect(-std::numeric_limits<Float>::max() * 0.5f,
+           -std::numeric_limits<Float>::max() * 0.5f,
+           std::numeric_limits<Float>::max(),
+           std::numeric_limits<Float>::max())),
+    Rect(-std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame * 0.5f,
+         -std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame * 0.5f,
+         std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame,
+         std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame));
   aRect.x = toDevPixels.x / devPixelsPerAppUnitToFrame;
   aRect.y = toDevPixels.y / devPixelsPerAppUnitToFrame;
   aRect.width = toDevPixels.width / devPixelsPerAppUnitToFrame;
   aRect.height = toDevPixels.height / devPixelsPerAppUnitToFrame;
   return TRANSFORM_SUCCEEDED;
 }
 
 nsRect
@@ -8000,8 +8008,55 @@ nsLayoutUtils::HasDocumentLevelListeners
     for (size_t i = 0; i < targets.Length(); i++) {
       if (HasApzAwareListeners(targets[i]->GetExistingListenerManager())) {
         return true;
       }
     }
   }
   return false;
 }
+
+/* static */ uint32_t
+nsLayoutUtils::GetTouchActionFromFrame(nsIFrame* aFrame)
+{
+  // If aFrame is null then return default value
+  if (!aFrame) {
+    return NS_STYLE_TOUCH_ACTION_AUTO;
+  }
+
+  // The touch-action CSS property applies to: all elements except:
+  // non-replaced inline elements, table rows, row groups, table columns, and column groups
+  bool isNonReplacedInlineElement = aFrame->IsFrameOfType(nsIFrame::eLineParticipant);
+  if (isNonReplacedInlineElement) {
+    return NS_STYLE_TOUCH_ACTION_AUTO;
+  }
+
+  const nsStyleDisplay* disp = aFrame->StyleDisplay();
+  bool isTableElement = disp->IsInnerTableStyle() &&
+    disp->mDisplay != NS_STYLE_DISPLAY_TABLE_CELL &&
+    disp->mDisplay != NS_STYLE_DISPLAY_TABLE_CAPTION;
+  if (isTableElement) {
+    return NS_STYLE_TOUCH_ACTION_AUTO;
+  }
+
+  return disp->mTouchAction;
+}
+
+/* static */  void
+nsLayoutUtils::TransformToAncestorAndCombineRegions(
+  const nsRect& aBounds,
+  nsIFrame* aFrame,
+  const nsIFrame* aAncestorFrame,
+  nsRegion* aPreciseTargetDest,
+  nsRegion* aImpreciseTargetDest)
+{
+  if (aBounds.IsEmpty()) {
+    return;
+  }
+  Matrix4x4 matrix = GetTransformToAncestor(aFrame, aAncestorFrame);
+  Matrix matrix2D;
+  bool isPrecise = (matrix.Is2D(&matrix2D)
+    && !matrix2D.HasNonAxisAlignedTransform());
+  nsRect transformed = TransformFrameRectToAncestor(
+    aFrame, aBounds, aAncestorFrame);
+  nsRegion* dest = isPrecise ? aPreciseTargetDest : aImpreciseTargetDest;
+  dest->OrWith(transformed);
+}
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -2413,16 +2413,35 @@ public:
    * Assert that the frame tree rooted at |aSubtreeRoot| is empty, i.e.,
    * that it contains no first-in-flows.
    */
   static void
   AssertTreeOnlyEmptyNextInFlows(nsIFrame *aSubtreeRoot);
 #endif
 
   /**
+   * Helper method to get touch action behaviour from the frame
+   */
+  static uint32_t
+  GetTouchActionFromFrame(nsIFrame* aFrame);
+
+  /**
+   * Helper method to transform |aBounds| from aFrame to aAncestorFrame,
+   * and combine it with |aPreciseTargetDest| if it is axis-aligned, or
+   * combine it with |aImpreciseTargetDest| if not.
+   */
+  static void
+  TransformToAncestorAndCombineRegions(
+    const nsRect& aBounds,
+    nsIFrame* aFrame,
+    const nsIFrame* aAncestorFrame,
+    nsRegion* aPreciseTargetDest,
+    nsRegion* aImpreciseTargetDest);
+
+  /**
    * Determine if aImageFrame (which is an nsImageFrame, nsImageControlFrame, or
    * nsSVGImageFrame) is visible or close to being visible via scrolling and
    * update the presshell with this knowledge.
    */
   static void
   UpdateImageVisibilityForFrame(nsIFrame* aImageFrame);
 
   /**
new file mode 100644
--- /dev/null
+++ b/mfbt/EnumeratedRange.h
@@ -0,0 +1,228 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/* Iterator over contiguous enum values */
+
+/*
+ * Implements generator functions that create a range to iterate over the values
+ * of a scoped or unscoped enum. Unlike IntegerRange, which can only function on
+ * the underlying integral type, the elements of the generated sequence will
+ * have the type of the enum in question.
+ *
+ * Note that the enum values should be contiguous in the iterated range;
+ * unfortunately there exists no way for EnumeratedRange to enforce this
+ * either dynamically or at compile time.
+ */
+
+#ifndef mozilla_EnumeratedRange_h
+#define mozilla_EnumeratedRange_h
+
+#include "mozilla/IntegerRange.h"
+#include "mozilla/IntegerTypeTraits.h"
+
+namespace mozilla {
+
+namespace detail {
+
+template<typename IntTypeT, typename EnumTypeT>
+class EnumeratedIterator
+{
+public:
+  typedef const EnumTypeT ValueType;
+  typedef typename MakeSigned<IntTypeT>::Type DifferenceType;
+
+  template<typename EnumType>
+  explicit EnumeratedIterator(EnumType aCurrent)
+    : mCurrent(aCurrent) { }
+
+  template<typename IntType, typename EnumType>
+  EnumeratedIterator(const EnumeratedIterator<IntType, EnumType>& aOther)
+    : mCurrent(aOther.mCurrent) { }
+
+  // Since operator* is required to return a reference, we return
+  // a reference to our member here.
+  const EnumTypeT& operator*() const { return mCurrent; }
+
+  /* Increment and decrement operators */
+
+  EnumeratedIterator& operator++()
+  {
+    mCurrent = EnumTypeT(IntTypeT(mCurrent) + IntTypeT(1));
+    return *this;
+  }
+  EnumeratedIterator& operator--()
+  {
+    mCurrent = EnumTypeT(IntTypeT(mCurrent) - IntTypeT(1));
+    return *this;
+  }
+  EnumeratedIterator operator++(int)
+  {
+    auto ret = *this;
+    mCurrent = EnumTypeT(IntTypeT(mCurrent) + IntTypeT(1));
+    return ret;
+  }
+  EnumeratedIterator operator--(int)
+  {
+    auto ret = *this;
+    mCurrent = EnumTypeT(IntTypeT(mCurrent) - IntTypeT(1));
+    return ret;
+  }
+
+  EnumeratedIterator operator+(DifferenceType aN) const
+  {
+    return EnumeratedIterator(EnumTypeT(IntTypeT(mCurrent) + aN));
+  }
+  EnumeratedIterator operator-(DifferenceType aN) const
+  {
+    return EnumeratedIterator(EnumTypeT(IntTypeT(mCurrent) - aN));
+  }
+  EnumeratedIterator& operator+=(DifferenceType aN)
+  {
+    mCurrent = EnumTypeT(IntTypeT(mCurrent) + aN);
+    return *this;
+  }
+  EnumeratedIterator& operator-=(DifferenceType aN)
+  {
+    mCurrent = EnumTypeT(IntTypeT(mCurrent) - aN);
+    return *this;
+  }
+
+  /* Comparison operators */
+
+  template<typename IntType, typename EnumType>
+  friend bool operator==(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                         const EnumeratedIterator<IntType, EnumType>& aIter2);
+  template<typename IntType, typename EnumType>
+  friend bool operator!=(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                         const EnumeratedIterator<IntType, EnumType>& aIter2);
+  template<typename IntType, typename EnumType>
+  friend bool operator<(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                        const EnumeratedIterator<IntType, EnumType>& aIter2);
+  template<typename IntType, typename EnumType>
+  friend bool operator<=(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                         const EnumeratedIterator<IntType, EnumType>& aIter2);
+  template<typename IntType, typename EnumType>
+  friend bool operator>(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                        const EnumeratedIterator<IntType, EnumType>& aIter2);
+  template<typename IntType, typename EnumType>
+  friend bool operator>=(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                         const EnumeratedIterator<IntType, EnumType>& aIter2);
+
+private:
+  EnumTypeT mCurrent;
+};
+
+template<typename IntType, typename EnumType>
+bool operator==(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                const EnumeratedIterator<IntType, EnumType>& aIter2)
+{
+  return aIter1.mCurrent == aIter2.mCurrent;
+}
+
+template<typename IntType, typename EnumType>
+bool operator!=(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                const EnumeratedIterator<IntType, EnumType>& aIter2)
+{
+  return aIter1.mCurrent != aIter2.mCurrent;
+}
+
+template<typename IntType, typename EnumType>
+bool operator<(const EnumeratedIterator<IntType, EnumType>& aIter1,
+               const EnumeratedIterator<IntType, EnumType>& aIter2)
+{
+  return aIter1.mCurrent < aIter2.mCurrent;
+}
+
+template<typename IntType, typename EnumType>
+bool operator<=(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                const EnumeratedIterator<IntType, EnumType>& aIter2)
+{
+  return aIter1.mCurrent <= aIter2.mCurrent;
+}
+
+template<typename IntType, typename EnumType>
+bool operator>(const EnumeratedIterator<IntType, EnumType>& aIter1,
+               const EnumeratedIterator<IntType, EnumType>& aIter2)
+{
+  return aIter1.mCurrent > aIter2.mCurrent;
+}
+
+template<typename IntType, typename EnumType>
+bool operator>=(const EnumeratedIterator<IntType, EnumType>& aIter1,
+                const EnumeratedIterator<IntType, EnumType>& aIter2)
+{
+  return aIter1.mCurrent >= aIter2.mCurrent;
+}
+
+template<typename IntTypeT, typename EnumTypeT>
+class EnumeratedRange
+{
+public:
+  typedef EnumeratedIterator<IntTypeT, EnumTypeT> iterator;
+  typedef EnumeratedIterator<IntTypeT, EnumTypeT> const_iterator;
+  typedef ReverseIterator<iterator> reverse_iterator;
+  typedef ReverseIterator<const_iterator> const_reverse_iterator;
+
+  template<typename EnumType>
+  EnumeratedRange(EnumType aBegin, EnumType aEnd)
+    : mBegin(aBegin), mEnd(aEnd) { }
+
+  iterator begin() const { return iterator(mBegin); }
+  const_iterator cbegin() const { return begin(); }
+  iterator end() const { return iterator(mEnd); }
+  const_iterator cend() const { return end(); }
+  reverse_iterator rbegin() const { return reverse_iterator(mEnd); }
+  const_reverse_iterator crbegin() const { return rbegin(); }
+  reverse_iterator rend() const { return reverse_iterator(mBegin); }
+  const_reverse_iterator crend() const { return rend(); }
+
+private:
+  EnumTypeT mBegin;
+  EnumTypeT mEnd;
+};
+
+} // namespace detail
+
+#ifdef __GNUC__
+// Enums can have an unsigned underlying type, which makes some of the
+// comparisons below always true or always false. Temporarily disable
+// -Wtype-limits to avoid breaking -Werror builds.
+#  pragma GCC diagnostic push
+#  pragma GCC diagnostic ignored "-Wtype-limits"
+#endif
+
+// Create a range to iterate from aBegin to aEnd, exclusive.
+template<typename IntType, typename EnumType>
+inline detail::EnumeratedRange<IntType, EnumType>
+MakeEnumeratedRange(EnumType aBegin, EnumType aEnd)
+{
+  typedef typename MakeUnsigned<IntType>::Type UnsignedType;
+  static_assert(sizeof(IntType) >= sizeof(EnumType),
+                "IntType should be at least as big as EnumType!");
+  MOZ_ASSERT(aBegin <= aEnd, "Cannot generate invalid, unbounded range!");
+  MOZ_ASSERT_IF(aBegin < EnumType(0), IsSigned<IntType>::value);
+  MOZ_ASSERT_IF(aBegin >= EnumType(0) && IsSigned<IntType>::value,
+                UnsignedType(aEnd) <= UnsignedType(MaxValue<IntType>::value));
+  return detail::EnumeratedRange<IntType, EnumType>(aBegin, aEnd);
+}
+
+// Create a range to iterate from EnumType(0) to aEnd, exclusive. EnumType(0)
+// should exist, but note that there is no way for us to ensure that it does!
+template<typename IntType, typename EnumType>
+inline detail::EnumeratedRange<IntType, EnumType>
+MakeEnumeratedRange(EnumType aEnd)
+{
+  return MakeEnumeratedRange<IntType>(EnumType(0), aEnd);
+}
+
+#ifdef __GNUC__
+#  pragma GCC diagnostic pop
+#endif
+
+} // namespace mozilla
+
+#endif // mozilla_EnumeratedRange_h
+
--- a/mfbt/IntegerRange.h
+++ b/mfbt/IntegerRange.h
@@ -28,20 +28,20 @@ public:
   explicit IntegerIterator(IntType aCurrent)
     : mCurrent(aCurrent) { }
 
   template<typename IntType>
   IntegerIterator(const IntegerIterator<IntType>& aOther)
     : mCurrent(aOther.mCurrent) { }
 
   // Since operator* is required to return a reference, we return
-  // the reference of our member here.
+  // a reference to our member here.
   const IntTypeT& operator*() const { return mCurrent; }
 
-  /* Increments and descrements operators */
+  /* Increment and decrement operators */
 
   IntegerIterator& operator++() { ++mCurrent; return *this; }
   IntegerIterator& operator--() { --mCurrent; return *this; }
   IntegerIterator operator++(int) { auto ret = *this; ++mCurrent; return ret; }
   IntegerIterator operator--(int) { auto ret = *this; --mCurrent; return ret; }
 
   IntegerIterator operator+(DifferenceType aN) const
   {
--- a/mfbt/moz.build
+++ b/mfbt/moz.build
@@ -27,16 +27,17 @@ EXPORTS.mozilla = [
     'Compression.h',
     'Constants.h',
     'DebugOnly.h',
     'decimal/Decimal.h',
     'double-conversion/double-conversion.h',
     'double-conversion/utils.h',
     'Endian.h',
     'EnumeratedArray.h',
+    'EnumeratedRange.h',
     'EnumSet.h',
     'FloatingPoint.h',
     'GuardObjects.h',
     'HashFunctions.h',
     'IntegerPrintfMacros.h',
     'IntegerRange.h',
     'IntegerTypeTraits.h',
     'IteratorTraits.h',
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -2983,16 +2983,17 @@ public class BrowserApp extends GeckoApp
         final boolean inGuestMode = GeckoProfile.get(this).inGuestMode();
 
         final boolean isAboutReader = AboutPages.isAboutReader(tab.getURL());
         bookmark.setEnabled(!isAboutReader);
         bookmark.setVisible(!inGuestMode);
         bookmark.setCheckable(true);
         bookmark.setChecked(tab.isBookmark());
         bookmark.setIcon(resolveBookmarkIconID(tab.isBookmark()));
+        bookmark.setTitle(resolveBookmarkTitleID(tab.isBookmark()));
 
         reader.setEnabled(isAboutReader || !AboutPages.isAboutPage(tab.getURL()));
         reader.setVisible(!inGuestMode);
         reader.setCheckable(true);
         final boolean isPageInReadingList = tab.isInReadingList();
         reader.setChecked(isPageInReadingList);
         reader.setIcon(resolveReadingListIconID(isPageInReadingList));
         reader.setTitle(resolveReadingListTitleID(isPageInReadingList));
@@ -3111,16 +3112,20 @@ public class BrowserApp extends GeckoApp
 
         if (isBookmark) {
             return R.drawable.ic_menu_bookmark_remove;
         } else {
             return R.drawable.ic_menu_bookmark_add;
         }
     }
 
+    private int resolveBookmarkTitleID(final boolean isBookmark) {
+        return (isBookmark ? R.string.bookmark_remove : R.string.bookmark);
+    }
+
     private int resolveReadingListIconID(final boolean isInReadingList) {
         return (isInReadingList ? R.drawable.ic_menu_reader_remove : R.drawable.ic_menu_reader_add);
     }
 
     private int resolveReadingListTitleID(final boolean isInReadingList) {
         return (isInReadingList ? R.string.reading_list_remove : R.string.overlay_share_reading_list_btn_label);
     }
 
@@ -3141,20 +3146,22 @@ public class BrowserApp extends GeckoApp
 
         if (itemId == R.id.bookmark) {
             tab = Tabs.getInstance().getSelectedTab();
             if (tab != null) {
                 if (item.isChecked()) {
                     Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, "bookmark");
                     tab.removeBookmark();
                     item.setIcon(resolveBookmarkIconID(false));
+                    item.setTitle(resolveBookmarkTitleID(false));
                 } else {
                     Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "bookmark");
                     tab.addBookmark();
                     item.setIcon(resolveBookmarkIconID(true));
+                    item.setTitle(resolveBookmarkTitleID(true));
                 }
             }
             return true;
         }
 
         if (itemId == R.id.reading_list) {
             tab = Tabs.getInstance().getSelectedTab();
             if (tab != null) {
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -37,16 +37,17 @@
 <!ENTITY  launcher_shortcuts_title "&brandShortName; Web Apps">
 <!ENTITY  launcher_shortcuts_empty "No web apps were found">
 
 <!ENTITY choose_file "Choose File">
 
 <!ENTITY url_bar_default_text2 "Search or enter address">
 
 <!ENTITY bookmark "Bookmark">
+<!ENTITY bookmark_remove "Remove bookmark">
 <!ENTITY bookmark_added "Bookmark added">
 <!ENTITY bookmark_removed "Bookmark removed">
 <!ENTITY bookmark_updated "Bookmark updated">
 <!ENTITY bookmark_options "Options">
 
 <!ENTITY history_today_section "Today">
 <!ENTITY history_yesterday_section "Yesterday">
 <!ENTITY history_week_section2 "Last Week">
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -73,16 +73,17 @@
   <string name="url_bar_default_text">&url_bar_default_text2;</string>
 
   <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/ -->
   <string name="help_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/</string>
   <string name="help_menu">&help_menu;</string>
 
   <string name="quit">&quit;</string>
   <string name="bookmark">&bookmark;</string>
+  <string name="bookmark_remove">&bookmark_remove;</string>
   <string name="bookmark_added">&bookmark_added;</string>
   <string name="bookmark_removed">&bookmark_removed;</string>
   <string name="bookmark_updated">&bookmark_updated;</string>
   <string name="bookmark_options">&bookmark_options;</string>
 
   <string name="history_today_section">&history_today_section;</string>
   <string name="history_yesterday_section">&history_yesterday_section;</string>
   <string name="history_week_section">&history_week_section2;</string>
--- a/mobile/android/base/tests/components/ToolbarComponent.java
+++ b/mobile/android/base/tests/components/ToolbarComponent.java
@@ -28,20 +28,19 @@ import com.jayway.android.robotium.solo.
 
 /**
  * A class representing any interactions that take place on the Toolbar.
  */
 public class ToolbarComponent extends BaseComponent {
 
     private static final String URL_HTTP_PREFIX = "http://";
 
-    // We are waiting up to 60 seconds instead of the default waiting time
-    // because reader mode parsing can take quite some time on slower devices
-    // See Bug 1142699
-    private static final int READER_MODE_WAIT_MS = 60000;
+    // We are waiting up to 30 seconds instead of the default waiting time because reader mode
+    // parsing can take quite some time on slower devices (Bug 1142699)
+    private static final int READER_MODE_WAIT_MS = 30000;
 
     public ToolbarComponent(final UITestContext testContext) {
         super(testContext);
     }
 
     public ToolbarComponent assertIsEditing() {
         fAssertTrue("The toolbar is in the editing state", isEditing());
         return this;
--- a/mobile/android/base/tests/roboextender/SelectionUtils.js
+++ b/mobile/android/base/tests/roboextender/SelectionUtils.js
@@ -20,16 +20,24 @@ const TYPE_NAME = "Robocop:testSelection
  *
  */
 function getSelectionHandler() {
   return (!this._selectionHandler) ?
     this._selectionHandler = Services.wm.getMostRecentWindow("navigator:browser").SelectionHandler :
     this._selectionHandler;
 }
 
+function getClipboard() {
+  return Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+}
+
+function getTextValue(aElement) {
+  return aElement.value || aElement.textContent;
+}
+
 function todo(result, msg) {
   return Messaging.sendRequestForResult({
     type: TYPE_NAME,
     todo: result,
     msg: msg
   });
 }
 
--- a/mobile/android/base/tests/roboextender/testSelectionHandler.html
+++ b/mobile/android/base/tests/roboextender/testSelectionHandler.html
@@ -4,32 +4,34 @@
     <meta name="viewport" content="initial-scale=1.0"/>
     <script type="application/javascript"
       src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
     <script type="application/javascript" src="SelectionUtils.js"></script>
     <script type="application/javascript;version=1.8">
 
 const DIV_POINT_TEXT = "Under";
 const INPUT_TEXT = "Text for select all in an <input>";
+const PASTE_TEXT = "Text for testing paste";
+const READONLY_INPUT_TEXT = "readOnly text";
 const TEXTAREA_TEXT = "Text for select all in a <textarea>";
-const READONLY_INPUT_TEXT = "readOnly text";
 
 /* =================================================================================
  *
  * Start of all text selection tests, check initialization state.
  *
  */
 function startTests() {
   testSelectAllDivs().
     then(testSelectDivAtPoint).
     then(testSelectInput).
     then(testSelectTextarea).
     then(testReadonlyInput).
     then(testCloseSelection).
     then(testStartSelectionFail).
+    then(testPaste).
 
     then(testAttachCaret).
     then(testAttachCaretFail).
 
     then(finishTests, function(err) {
       ok(false, "Error in selection test " + err);
       finishTests();
     });
@@ -346,16 +348,54 @@ function testAttachCaretFail() {
       is(attachCaretResult, sh.ATTACH_ERROR_INCOMPATIBLE,
         "attachCaret() should have failed predictably."),
       is(sh._activeType, sh.TYPE_NONE,
         "Selection should not be active at end of testAttachCaretFail."),
     ]);
   });
 }
 
+/* =================================================================================
+ *
+ * Tests to ensure we can paste text inside editable elements
+ *
+ */
+function testPaste() {
+  let sh = getSelectionHandler();
+  let clipboard = getClipboard();
+  clipboard.copyString(PASTE_TEXT, document);
+
+  // Add a contentEditable element to the document.
+  let div = document.createElement("div");
+  div.contentEditable = true;
+  div.dataset.editable = true;
+  document.body.appendChild(div);
+
+  let elements = document.querySelectorAll("div, input, textarea");
+  let promises = [];
+
+  for (var i = 0; i < elements.length; i++) {
+    sh.startSelection(elements[i]);
+    if (sh.isElementEditableText(elements[i]) && !elements[i].disabled) {
+      sh.actions.PASTE.action(elements[i]);
+    }
+    if (elements[i].dataset.editable) {
+      promises.push(is(getTextValue(elements[i]), PASTE_TEXT, "Pasted correctly"));
+      promises.push(ok(sh.isElementEditableText(elements[i]), "Element is editable"));
+    } else {
+      promises.push(isNot(getTextValue(elements[i]), PASTE_TEXT, "Paste failed correctly"));
+    }
+  }
+
+  document.body.removeChild(div);
+  div = null;
+
+  return Promise.all(promises);
+}
+
     </script>
   </head>
 
   <body onload="startTests();">
 
     <div id="selDiv">Under sufficiently extreme conditions, quarks may become
       deconfined and exist as free particles. In the course of asymptotic freedom,
       the strong interaction becomes weaker at higher temperatures. Eventually,
@@ -376,19 +416,19 @@ function testAttachCaretFail() {
       platea dictumst. Sed placerat tellus quis lacus condimentum, quis luctus elit
       pellentesque. Mauris cursus neque diam, sit amet gravida quam porta ac.
       Aliquam aliquam feugiat vestibulum. Proin commodo nulla ligula, in bibendum
       massa euismod a. Ut ac lobortis dui. Ut id augue id arcu ornare suscipit eu
       ornare lorem. Pellentesque nec dictum ante. Nam quis ligula ultricies, auctor
       nunc vel, fringilla turpis. Nulla lacinia, leo ut egestas hendrerit, risus
       ligula interdum enim, vel varius libero sem ut ligula.</div><br>
 
-    <input id="inputNode" type="text"><br>
+    <input data-editable="true" id="inputNode" type="text"><br>
 
-    <textarea id="textareaNode"></textarea><br>
+    <textarea data-editable="true" id="textareaNode"></textarea><br>
 
     <input id="readOnlyTextInput" type="text" readonly><br>
 
     <input id="inputButton" type="button" value="Click me"><br>
 
     <input id="inputDisabled" type="text" disabled><br>
   </body>
 </html>
--- a/mobile/android/base/tests/testReaderModeTitle.java
+++ b/mobile/android/base/tests/testReaderModeTitle.java
@@ -1,16 +1,19 @@
 package org.mozilla.gecko.tests;
 
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
 import org.mozilla.gecko.tests.helpers.NavigationHelper;
 
 /**
  * This tests ensures that the toolbar in reader mode displays the original page url.
  */
 public class testReaderModeTitle extends UITest {
     public void testReaderModeTitle() {
+        GeckoHelper.blockForReady();
+
         NavigationHelper.enterAndLoadUrl(StringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE);
 
         mToolbar.pressReaderModeButton();
 
         mToolbar.assertTitle(StringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE);
     }
 }
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -674,17 +674,18 @@ var SelectionHandler = {
 
         // copySelection closes the selection. Show a caret where we just cut the text.
         SelectionHandler.attachCaret(aElement);
         UITelemetry.addEvent("action.1", "actionbar", null, "cut");
       },
       order: 4,
       selector: {
         matches: function(aElement) {
-          return SelectionHandler.isElementEditableText(aElement) ?
+          // Disallow cut for contentEditable elements (until Bug 1112276 is fixed).
+          return !aElement.isContentEditable && SelectionHandler.isElementEditableText(aElement) ?
             SelectionHandler.isSelectionActive() : false;
         }
       }
     },
 
     COPY: {
       label: Strings.browser.GetStringFromName("contextmenu.copy"),
       id: "copy_action",
@@ -706,20 +707,20 @@ var SelectionHandler = {
       }
     },
 
     PASTE: {
       label: Strings.browser.GetStringFromName("contextmenu.paste"),
       id: "paste_action",
       icon: "drawable://ab_paste",
       action: function(aElement) {
-        if (aElement && (aElement instanceof Ci.nsIDOMNSEditableElement)) {
-          let target = aElement.QueryInterface(Ci.nsIDOMNSEditableElement);
-          target.editor.paste(Ci.nsIClipboard.kGlobalClipboard);
-          target.focus();
+        if (aElement) {
+          let target = SelectionHandler._getEditor();
+          aElement.focus();
+          target.paste(Ci.nsIClipboard.kGlobalClipboard);
           SelectionHandler._closeSelection();
           UITelemetry.addEvent("action.1", "actionbar", null, "paste");
         }
       },
       order: 2,
       selector: {
         matches: function(aElement) {
           if (SelectionHandler.isElementEditableText(aElement)) {
@@ -890,17 +891,18 @@ var SelectionHandler = {
 
   // Used by the contextmenu "matches" functions in ClipboardHelper
   isSelectionActive: function sh_isSelectionActive() {
     return (this._activeType == this.TYPE_SELECTION);
   },
 
   isElementEditableText: function (aElement) {
     return (((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
-            (aElement instanceof HTMLTextAreaElement)) && !aElement.readOnly);
+            (aElement instanceof HTMLTextAreaElement)) && !aElement.readOnly) ||
+            aElement.isContentEditable;
   },
 
   _isNonTextInputElement: function(aElement) {
     return (aElement instanceof HTMLInputElement && !aElement.mozIsTextField(false));
   },
 
   /*
    * Moves the selection as the user drags a handle.
@@ -958,17 +960,17 @@ var SelectionHandler = {
     } else {
       selection.extend(caretPos.offsetNode, caretPos.offset);
     }
   },
 
   _moveCaret: function sh_moveCaret(aX, aY) {
     // Get rect of text inside element
     let range = document.createRange();
-    range.selectNodeContents(this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.rootElement);
+    range.selectNodeContents(this._getEditor().rootElement);
     let textBounds = range.getBoundingClientRect();
 
     // Get rect of editor
     let editorBounds = this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_EDITOR_RECT, 0, 0, 0, 0,
                                                                this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
     // the return value from sendQueryContentEvent is in LayoutDevice pixels and we want CSS pixels, so
     // divide by the pixel ratio
     let editorRect = new Rect(editorBounds.left / window.devicePixelRatio,
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4472,17 +4472,21 @@ pref("image.mozsamplesize.enabled", fals
 pref("beacon.enabled", true);
 #endif
 
 // Camera prefs
 pref("camera.control.face_detection.enabled", true);
 
 
 // SW Cache API
+#ifdef RELEASE_BUILD
 pref("dom.caches.enabled", false);
+#else
+pref("dom.caches.enabled", true);
+#endif // RELEASE_BUILD
 
 #ifdef MOZ_WIDGET_GONK
 // Empirically, this is the value returned by hal::GetTotalSystemMemory()
 // when Flame's memory is limited to 512MiB. If the camera stack determines
 // it is running on a low memory platform, features that can be reliably
 // supported will be disabled. This threshold can be adjusted to suit other
 // platforms; and set to 0 to disable the low-memory check altogether.
 pref("camera.control.low_memory_thresholdMB", 404);
new file mode 100644
--- /dev/null
+++ b/python/compare-locales/compare_locales/__init__.py
@@ -0,0 +1,1 @@
+version = "0.10.1a"
new file mode 100644
--- /dev/null
+++ b/python/compare-locales/compare_locales/checks.py
@@ -0,0 +1,420 @@
+# 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/.
+
+import re
+from difflib import SequenceMatcher
+from xml import sax
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+from compare_locales.parser import DTDParser, PropertiesParser
+
+
+class Checker(object):
+    '''Abstract class to implement checks per file type.
+    '''
+    pattern = None
+
+    @classmethod
+    def use(cls, file):
+        return cls.pattern.match(file.file)
+
+    def check(self, refEnt, l10nEnt):
+        '''Given the reference and localized Entities, performs checks.
+
+        This is a generator yielding tuples of
+        - "warning" or "error", depending on what should be reported,
+        - tuple of line, column info for the error within the string
+        - description string to be shown in the report
+        '''
+        if True:
+            raise NotImplementedError("Need to subclass")
+        yield ("error", (0, 0), "This is an example error", "example")
+
+
+class PrintfException(Exception):
+    def __init__(self, msg, pos):
+        self.pos = pos
+        self.msg = msg
+
+
+class PropertiesChecker(Checker):
+    '''Tests to run on .properties files.
+    '''
+    pattern = re.compile('.*\.properties$')
+    printf = re.compile(r'%(?P<good>%|'
+                        r'(?:(?P<number>[1-9][0-9]*)\$)?'
+                        r'(?P<width>\*|[0-9]+)?'
+                        r'(?P<prec>\.(?:\*|[0-9]+)?)?'
+                        r'(?P<spec>[duxXosScpfg]))?')
+
+    def check(self, refEnt, l10nEnt):
+        '''Test for the different variable formats.
+        '''
+        refValue, l10nValue = refEnt.val, l10nEnt.val
+        refSpecs = None
+        # check for PluralForm.jsm stuff, should have the docs in the
+        # comment
+        if 'Localization_and_Plurals' in refEnt.pre_comment:
+            # For plurals, common variable pattern is #1. Try that.
+            pats = set(int(m.group(1)) for m in re.finditer('#([0-9]+)',
+                                                            refValue))
+            if len(pats) == 0:
+                return
+            lpats = set(int(m.group(1)) for m in re.finditer('#([0-9]+)',
+                                                             l10nValue))
+            if pats - lpats:
+                yield ('warning', 0, 'not all variables used in l10n',
+                       'plural')
+                return
+            if lpats - pats:
+                yield ('error', 0, 'unreplaced variables in l10n',
+                       'plural')
+                return
+            return
+        # check for lost escapes
+        raw_val = l10nEnt.raw_val
+        for m in PropertiesParser.escape.finditer(raw_val):
+            if m.group('single') and \
+               m.group('single') not in PropertiesParser.known_escapes:
+                yield ('warning', m.start(),
+                       'unknown escape sequence, \\' + m.group('single'),
+                       'escape')
+        try:
+            refSpecs = self.getPrintfSpecs(refValue)
+        except PrintfException:
+            refSpecs = []
+        if refSpecs:
+            for t in self.checkPrintf(refSpecs, l10nValue):
+                yield t
+            return
+
+    def checkPrintf(self, refSpecs, l10nValue):
+        try:
+            l10nSpecs = self.getPrintfSpecs(l10nValue)
+        except PrintfException, e:
+            yield ('error', e.pos, e.msg, 'printf')
+            return
+        if refSpecs != l10nSpecs:
+            sm = SequenceMatcher()
+            sm.set_seqs(refSpecs, l10nSpecs)
+            msgs = []
+            warn = None
+            for action, i1, i2, j1, j2 in sm.get_opcodes():
+                if action == 'equal':
+                    continue
+                if action == 'delete':
+                    # missing argument in l10n
+                    if i2 == len(refSpecs):
+                        # trailing specs missing, that's just a warning
+                        warn = ', '.join('trailing argument %d `%s` missing' %
+                                         (i+1, refSpecs[i])
+                                         for i in xrange(i1, i2))
+                    else:
+                        for i in xrange(i1, i2):
+                            msgs.append('argument %d `%s` missing' %
+                                        (i+1, refSpecs[i]))
+                    continue
+                if action == 'insert':
+                    # obsolete argument in l10n
+                    for i in xrange(j1, j2):
+                        msgs.append('argument %d `%s` obsolete' %
+                                    (i+1, l10nSpecs[i]))
+                    continue
+                if action == 'replace':
+                    for i, j in zip(xrange(i1, i2), xrange(j1, j2)):
+                        msgs.append('argument %d `%s` should be `%s`' %
+                                    (j+1, l10nSpecs[j], refSpecs[i]))
+            if msgs:
+                yield ('error', 0, ', '.join(msgs), 'printf')
+            if warn is not None:
+                yield ('warning', 0, warn, 'printf')
+
+    def getPrintfSpecs(self, val):
+        hasNumber = False
+        specs = []
+        for m in self.printf.finditer(val):
+            if m.group("good") is None:
+                # found just a '%', signal an error
+                raise PrintfException('Found single %', m.start())
+            if m.group("good") == '%':
+                # escaped %
+                continue
+            if ((hasNumber and m.group('number') is None) or
+                    (not hasNumber and specs and
+                     m.group('number') is not None)):
+                # mixed style, numbered and not
+                raise PrintfException('Mixed ordered and non-ordered args',
+                                      m.start())
+            hasNumber = m.group('number') is not None
+            if hasNumber:
+                pos = int(m.group('number')) - 1
+                ls = len(specs)
+                if pos >= ls:
+                    # pad specs
+                    nones = pos - ls
+                    specs[ls:pos] = nones*[None]
+                    specs.append(m.group('spec'))
+                else:
+                    if specs[pos] is not None:
+                        raise PrintfException('Double ordered argument %d' %
+                                              (pos+1),
+                                              m.start())
+                    specs[pos] = m.group('spec')
+            else:
+                specs.append(m.group('spec'))
+        # check for missing args
+        if hasNumber and not all(specs):
+            raise PrintfException('Ordered argument missing', 0)
+        return specs
+
+
+class DTDChecker(Checker):
+    """Tests to run on DTD files.
+
+    Uses xml.sax for the heavy lifting of xml parsing.
+
+    The code tries to parse until it doesn't find any unresolved entities
+    anymore. If it finds one, it tries to grab the key, and adds an empty
+    <!ENTITY key ""> definition to the header.
+
+    Also checks for some CSS and number heuristics in the values.
+    """
+    pattern = re.compile('.*\.dtd$')
+
+    eref = re.compile('&(%s);' % DTDParser.Name)
+    tmpl = '''<!DOCTYPE elem [%s]>
+<elem>%s</elem>
+'''
+    xmllist = set(('amp', 'lt', 'gt', 'apos', 'quot'))
+
+    def __init__(self, reference):
+        self.reference = reference
+        self.__known_entities = None
+
+    def known_entities(self, refValue):
+        if self.__known_entities is None and self.reference is not None:
+            self.__known_entities = set()
+            for ent in self.reference:
+                self.__known_entities.update(self.entities_for_value(ent.val))
+        return self.__known_entities if self.__known_entities is not None \
+            else self.entities_for_value(refValue)
+
+    def entities_for_value(self, value):
+        reflist = set(m.group(1).encode('utf-8')
+                      for m in self.eref.finditer(value))
+        reflist -= self.xmllist
+        return reflist
+
+    # Setup for XML parser, with default and text-only content handler
+    class TextContent(sax.handler.ContentHandler):
+        textcontent = ''
+
+        def characters(self, content):
+            self.textcontent += content
+
+    defaulthandler = sax.handler.ContentHandler()
+    texthandler = TextContent()
+
+    numPattern = r'([0-9]+|[0-9]*\.[0-9]+)'
+    num = re.compile('^%s$' % numPattern)
+    lengthPattern = '%s(em|px|ch|cm|in)' % numPattern
+    length = re.compile('^%s$' % lengthPattern)
+    spec = re.compile(r'((?:min\-)?(?:width|height))\s*:\s*%s' %
+                      lengthPattern)
+    style = re.compile(r'^%(spec)s\s*(;\s*%(spec)s\s*)*;?$' %
+                       {'spec': spec.pattern})
+
+    processContent = None
+
+    def check(self, refEnt, l10nEnt):
+        """Try to parse the refvalue inside a dummy element, and keep
+        track of entities that we need to define to make that work.
+
+        Return a checker that offers just those entities.
+        """
+        refValue, l10nValue = refEnt.val, l10nEnt.val
+        # find entities the refValue references,
+        # reusing markup from DTDParser.
+        reflist = self.known_entities(refValue)
+        entities = ''.join('<!ENTITY %s "">' % s for s in sorted(reflist))
+        parser = sax.make_parser()
+        parser.setFeature(sax.handler.feature_external_ges, False)
+
+        parser.setContentHandler(self.defaulthandler)
+        try:
+            parser.parse(StringIO(self.tmpl %
+                                  (entities, refValue.encode('utf-8'))))
+            # also catch stray %
+            parser.parse(StringIO(self.tmpl %
+                                  (refEnt.all.encode('utf-8') + entities,
+                                   '&%s;' % refEnt.key.encode('utf-8'))))
+        except sax.SAXParseException, e:
+            yield ('warning',
+                   (0, 0),
+                   "can't parse en-US value", 'xmlparse')
+
+        # find entities the l10nValue references,
+        # reusing markup from DTDParser.
+        l10nlist = self.entities_for_value(l10nValue)
+        missing = sorted(l10nlist - reflist)
+        _entities = entities + ''.join('<!ENTITY %s "">' % s for s in missing)
+        warntmpl = u'Referencing unknown entity `%s`'
+        if reflist:
+            warntmpl += ' (%s known)' % ', '.join(sorted(reflist))
+        if self.processContent is not None:
+            self.texthandler.textcontent = ''
+            parser.setContentHandler(self.texthandler)
+        try:
+            parser.parse(StringIO(self.tmpl % (_entities,
+                         l10nValue.encode('utf-8'))))
+            # also catch stray %
+            # if this fails, we need to substract the entity definition
+            parser.setContentHandler(self.defaulthandler)
+            parser.parse(StringIO(self.tmpl % (
+                l10nEnt.all.encode('utf-8') + _entities,
+                '&%s;' % l10nEnt.key.encode('utf-8'))))
+        except sax.SAXParseException, e:
+            # xml parse error, yield error
+            # sometimes, the error is reported on our fake closing
+            # element, make that the end of the last line
+            lnr = e.getLineNumber() - 1
+            lines = l10nValue.splitlines()
+            if lnr > len(lines):
+                lnr = len(lines)
+                col = len(lines[lnr-1])
+            else:
+                col = e.getColumnNumber()
+                if lnr == 1:
+                    # first line starts with <elem>, substract
+                    col -= len("<elem>")
+                elif lnr == 0:
+                    col -= len("<!DOCTYPE elem [")  # first line is DOCTYPE
+            yield ('error', (lnr, col), ' '.join(e.args), 'xmlparse')
+
+        for key in missing:
+            yield ('warning', (0, 0), warntmpl % key.decode('utf-8'),
+                   'xmlparse')
+
+        # Number check
+        if self.num.match(refValue) and not self.num.match(l10nValue):
+            yield ('warning', 0, 'reference is a number', 'number')
+        # CSS checks
+        # just a length, width="100em"
+        if self.length.match(refValue) and not self.length.match(l10nValue):
+            yield ('error', 0, 'reference is a CSS length', 'css')
+        # real CSS spec, style="width:100px;"
+        if self.style.match(refValue):
+            if not self.style.match(l10nValue):
+                yield ('error', 0, 'reference is a CSS spec', 'css')
+            else:
+                # warn if different properties or units
+                refMap = dict((s, u) for s, _, u in
+                              self.spec.findall(refValue))
+                msgs = []
+                for s, _, u in self.spec.findall(l10nValue):
+                    if s not in refMap:
+                        msgs.insert(0, '%s only in l10n' % s)
+                        continue
+                    else:
+                        ru = refMap.pop(s)
+                        if u != ru:
+                            msgs.append("units for %s don't match "
+                                        "(%s != %s)" % (s, u, ru))
+                for s in refMap.iterkeys():
+                    msgs.insert(0, '%s only in reference' % s)
+                if msgs:
+                    yield ('warning', 0, ', '.join(msgs), 'css')
+
+        if self.processContent is not None:
+            for t in self.processContent(self.texthandler.textcontent):
+                yield t
+
+
+class PrincessAndroid(DTDChecker):
+    """Checker for the string values that Android puts into an XML container.
+
+    http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling  # noqa
+    has more info. Check for unescaped apostrophes and bad unicode escapes.
+    """
+    quoted = re.compile("(?P<q>[\"']).*(?P=q)$")
+
+    def unicode_escape(self, str):
+        """Helper method to try to decode all unicode escapes in a string.
+
+        This code uses the standard python decode for unicode-escape, but
+        that's somewhat tricky, as its input needs to be ascii. To get to
+        ascii, the unicode string gets converted to ascii with
+        backslashreplace, i.e., all non-ascii unicode chars get unicode
+        escaped. And then we try to roll all of that back.
+        Now, when that hits an error, that's from the original string, and we
+        need to search for the actual error position in the original string,
+        as the backslashreplace code changes string positions quite badly.
+        See also the last check in TestAndroid.test_android_dtd, with a
+        lengthy chinese string.
+        """
+        val = str.encode('ascii', 'backslashreplace')
+        try:
+            val.decode('unicode-escape')
+        except UnicodeDecodeError, e:
+            args = list(e.args)
+            badstring = args[1][args[2]:args[3]]
+            i = len(args[1][:args[2]].decode('unicode-escape'))
+            args[2] = i
+            args[3] = i + len(badstring)
+            raise UnicodeDecodeError(*args)
+
+    @classmethod
+    def use(cls, file):
+        """Use this Checker only for DTD files in embedding/android."""
+        return (file.module in ("embedding/android",
+                                "mobile/android/base")
+                and cls.pattern.match(file.file))
+
+    def processContent(self, val):
+        """Actual check code.
+        Check for unicode escapes and unescaped quotes and apostrophes,
+        if string's not quoted.
+        """
+        # first, try to decode unicode escapes
+        try:
+            self.unicode_escape(val)
+        except UnicodeDecodeError, e:
+            yield ('error', e.args[2], e.args[4], 'android')
+        # check for unescaped single or double quotes.
+        # first, see if the complete string is single or double quoted,
+        # that changes the rules
+        m = self.quoted.match(val)
+        if m:
+            q = m.group('q')
+            offset = 0
+            val = val[1:-1]  # strip quotes
+        else:
+            q = "[\"']"
+            offset = -1
+        stray_quot = re.compile(r"[\\\\]*(%s)" % q)
+
+        for m in stray_quot.finditer(val):
+            if len(m.group(0)) % 2:
+                # found an unescaped single or double quote, which message?
+                if m.group(1) == '"':
+                    msg = u"Quotes in Android DTDs need escaping with \\\" "\
+                          u"or \\u0022, or put string in apostrophes."
+                else:
+                    msg = u"Apostrophes in Android DTDs need escaping with "\
+                          u"\\' or \\u0027, or use \u2019, or put string in "\
+                          u"quotes."
+                yield ('error', m.end(0)+offset, msg, 'android')
+
+
+def getChecker(file, reference=None):
+    if PropertiesChecker.use(file):
+        return PropertiesChecker()
+    if PrincessAndroid.use(file):
+        return PrincessAndroid(reference)
+    if DTDChecker.use(file):
+        return DTDChecker(reference)
+    return None
new file mode 100644
--- /dev/null
+++ b/python/compare-locales/compare_locales/commands.py
@@ -0,0 +1,154 @@
+# 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/.
+
+'Commands exposed to commandlines'
+
+import logging
+from optparse import OptionParser, make_option
+
+from compare_locales.paths import EnumerateApp
+from compare_locales.compare import compareApp, compareDirs
+from compare_locales.webapps import compare_web_app
+
+
+class BaseCommand(object):
+    """Base class for compare-locales commands.
+    This handles command line parsing, and general sugar for setuptools
+    entry_points.
+    """
+    options = [
+        make_option('-v', '--verbose', action='count', dest='v', default=0,
+                    help='Make more noise'),
+        make_option('-q', '--quiet', action='count', dest='q', default=0,
+                    help='Make less noise'),
+        make_option('-m', '--merge',
+                    help='''Use this directory to stage merged files,
+use {ab_CD} to specify a different directory for each locale'''),
+    ]
+    data_option = make_option('--data', choices=['text', 'exhibit', 'json'],
+                              default='text',
+                              help='''Choose data and format (one of text,
+exhibit, json); text: (default) Show which files miss which strings, together
+with warnings and errors. Also prints a summary; json: Serialize the internal
+tree, useful for tools. Also always succeeds; exhibit: Serialize the summary
+data in a json useful for Exhibit
+''')
+
+    def __init__(self):
+        self.parser = None
+
+    def get_parser(self):
+        """Get an OptionParser, with class docstring as usage, and
+        self.options.
+        """
+        parser = OptionParser()
+        parser.set_usage(self.__doc__)
+        for option in self.options:
+            parser.add_option(option)
+        return parser
+
+    @classmethod
+    def call(cls):
+        """Entry_point for setuptools.
+        The actual command handling is done in the handle() method of the
+        subclasses.
+        """
+        cmd = cls()
+        cmd.handle_()
+
+    def handle_(self):
+        """The instance part of the classmethod call."""
+        self.parser = self.get_parser()
+        (options, args) = self.parser.parse_args()
+        # log as verbose or quiet as we want, warn by default
+        logging.basicConfig()
+        logging.getLogger().setLevel(logging.WARNING -
+                                     (options.v - options.q)*10)
+        observer = self.handle(args, options)
+        print observer.serialize(type=options.data).encode('utf-8', 'replace')
+
+    def handle(self, args, options):
+        """Subclasses need to implement this method for the actual
+        command handling.
+        """
+        raise NotImplementedError
+
+
+class CompareLocales(BaseCommand):
+    """usage: %prog [options] l10n.ini l10n_base_dir [locale ...]
+
+Check the localization status of a gecko application.
+The first argument is a path to the l10n.ini file for the application,
+followed by the base directory of the localization repositories.
+Then you pass in the list of locale codes you want to compare. If there are
+not locales given, the list of locales will be taken from the all-locales file
+of the application\'s l10n.ini."""
+
+    options = BaseCommand.options + [
+        make_option('--clobber-merge', action="store_true", default=False,
+                    dest='clobber',
+                    help="""WARNING: DATALOSS.
+Use this option with care. If specified, the merge directory will
+be clobbered for each module. That means, the subdirectory will
+be completely removed, any files that were there are lost.
+Be careful to specify the right merge directory when using this option."""),
+        make_option('-r', '--reference', default='en-US', dest='reference',
+                    help='Explicitly set the reference '
+                    'localization. [default: en-US]'),
+        BaseCommand.data_option
+    ]
+
+    def handle(self, args, options):
+        if len(args) < 2:
+            self.parser.error('Need to pass in list of languages')
+        inipath, l10nbase = args[:2]
+        locales = args[2:]
+        app = EnumerateApp(inipath, l10nbase, locales)
+        app.reference = options.reference
+        try:
+            observer = compareApp(app, merge_stage=options.merge,
+                                  clobber=options.clobber)
+        except (OSError, IOError), exc:
+            print "FAIL: " + str(exc)
+            self.parser.exit(2)
+        return observer
+
+
+class CompareDirs(BaseCommand):
+    """usage: %prog [options] reference localization
+
+Check the localization status of a directory tree.
+The first argument is a path to the reference data,the second is the
+localization to be tested."""
+
+    options = BaseCommand.options + [
+        BaseCommand.data_option
+    ]
+
+    def handle(self, args, options):
+        if len(args) != 2:
+            self.parser.error('Reference and localizatino required')
+        reference, locale = args
+        observer = compareDirs(reference, locale, merge_stage=options.merge)
+        return observer
+
+
+class CompareWebApp(BaseCommand):
+    """usage: %prog [options] webapp [locale locale]
+
+Check the localization status of a gaia-style web app.
+The first argument is the directory of the web app.
+Following arguments explicitly state the locales to test.
+If none are given, test all locales in manifest.webapp or files."""
+
+    options = BaseCommand.options[:-1] + [
+        BaseCommand.data_option]
+
+    def handle(self, args, options):
+        if len(args) < 1:
+            self.parser.error('Webapp directory required')
+        basedir = args[0]
+        locales = args[1:]
+        observer = compare_web_app(basedir, locales)
+        return observer
new file mode 100644
--- /dev/null
+++ b/python/compare-locales/compare_locales/compare.py
@@ -0,0 +1,635 @@
+# 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/.
+
+'Mozilla l10n compare locales tool'
+
+import codecs
+import os
+import os.path
+import shutil
+import re
+from difflib import SequenceMatcher
+from collections import defaultdict
+
+try:
+    from json import dumps
+except:
+    from simplejson import dumps
+
+from compare_locales import parser
+from compare_locales import paths
+from compare_locales.checks import getChecker
+
+
+class Tree(object):
+    def __init__(self, valuetype):
+        self.branches = dict()
+        self.valuetype = valuetype
+        self.value = None
+
+    def __getitem__(self, leaf):
+        parts = []
+        if isinstance(leaf, paths.File):
+            parts = [p for p in [leaf.locale, leaf.module] if p] + \
+                leaf.file.split('/')
+        else:
+            parts = leaf.split('/')
+        return self.__get(parts)
+
+    def __get(self, parts):
+        common = None
+        old = None
+        new = tuple(parts)
+        t = self
+        for k, v in self.branches.iteritems():
+            for i, part in enumerate(zip(k, parts)):
+                if part[0] != part[1]:
+                    i -= 1
+                    break
+            if i < 0:
+                continue
+            i += 1
+            common = tuple(k[:i])
+            old = tuple(k[i:])
+            new = tuple(parts[i:])
+            break
+        if old:
+            self.branches.pop(k)
+            t = Tree(self.valuetype)
+            t.branches[old] = v
+            self.branches[common] = t
+        elif common:
+            t = self.branches[common]
+        if new:
+            if common:
+                return t.__get(new)
+            t2 = t
+            t = Tree(self.valuetype)
+            t2.branches[new] = t
+        if t.value is None:
+            t.value = t.valuetype()
+        return t.value
+
+    indent = '  '
+
+    def getContent(self, depth=0):
+        '''
+        Returns iterator of (depth, flag, key_or_value) tuples.
+        If flag is 'value', key_or_value is a value object, otherwise
+        (flag is 'key') it's a key string.
+        '''
+        keys = self.branches.keys()
+        keys.sort()
+        if self.value is not None:
+            yield (depth, 'value', self.value)
+        for key in keys:
+            yield (depth, 'key', key)
+            for child in self.branches[key].getContent(depth + 1):
+                yield child
+
+    def toJSON(self):
+        '''
+        Returns this Tree as a JSON-able tree of hashes.
+        Only the values need to take care that they're JSON-able.
+        '''
+        json = {}
+        keys = self.branches.keys()
+        keys.sort()
+        if self.value is not None:
+            json['value'] = self.value
+        children = [('/'.join(key), self.branches[key].toJSON())
+                    for key in keys]
+        if children:
+            json['children'] = children
+        return json
+
+    def getStrRows(self):
+        def tostr(t):
+            if t[1] == 'key':
+                return self.indent * t[0] + '/'.join(t[2])
+            return self.indent * (t[0] + 1) + str(t[2])
+
+        return map(tostr, self.getContent())
+
+    def __str__(self):
+        return '\n'.join(self.getStrRows())
+
+
+class AddRemove(SequenceMatcher):
+    def __init__(self):
+        SequenceMatcher.__init__(self, None, None, None)
+
+    def set_left(self, left):
+        if not isinstance(left, list):
+            left = [l for l in left]
+        self.set_seq1(left)
+
+    def set_right(self, right):
+        if not isinstance(right, list):
+            right = [l for l in right]
+        self.set_seq2(right)
+
+    def __iter__(self):
+        for tag, i1, i2, j1, j2 in self.get_opcodes():
+            if tag == 'equal':
+                for pair in zip(self.a[i1:i2], self.b[j1:j2]):
+                    yield ('equal', pair)
+            elif tag == 'delete':
+                for item in self.a[i1:i2]:
+                    yield ('delete', item)
+            elif tag == 'insert':
+                for item in self.b[j1:j2]:
+                    yield ('add', item)
+            else:
+                # tag == 'replace'
+                for item in self.a[i1:i2]:
+                    yield ('delete', item)
+                for item in self.b[j1:j2]:
+                    yield ('add', item)
+
+
+class DirectoryCompare(SequenceMatcher):
+    def __init__(self, reference):
+        SequenceMatcher.__init__(self, None, [i for i in reference],
+                                 [])
+        self.watcher = None
+
+    def setWatcher(self, watcher):
+        self.watcher = watcher
+
+    def compareWith(self, other):
+        if not self.watcher:
+            return
+        self.set_seq2([i for i in other])
+        for tag, i1, i2, j1, j2 in self.get_opcodes():
+            if tag == 'equal':
+                for i, j in zip(xrange(i1, i2), xrange(j1, j2)):
+                    self.watcher.compare(self.a[i], self.b[j])
+            elif tag == 'delete':
+                for i in xrange(i1, i2):
+                    self.watcher.add(self.a[i], other.cloneFile(self.a[i]))
+            elif tag == 'insert':
+                for j in xrange(j1, j2):
+                    self.watcher.remove(self.b[j])
+            else:
+                for j in xrange(j1, j2):
+                    self.watcher.remove(self.b[j])
+                for i in xrange(i1, i2):
+                    self.watcher.add(self.a[i], other.cloneFile(self.a[i]))
+
+
+class Observer(object):
+    stat_cats = ['missing', 'obsolete', 'missingInFiles', 'report',
+                 'changed', 'unchanged', 'keys']
+
+    def __init__(self):
+        class intdict(defaultdict):
+            def __init__(self):
+                defaultdict.__init__(self, int)
+
+        self.summary = defaultdict(intdict)
+        self.details = Tree(dict)
+        self.filter = None
+
+    # support pickling
+    def __getstate__(self):
+        return dict(summary=self.getSummary(), details=self.details)
+
+    def __setstate__(self, state):
+        class intdict(defaultdict):
+            def __init__(self):
+                defaultdict.__init__(self, int)
+
+        self.summary = defaultdict(intdict)
+        if 'summary' in state:
+            for loc, stats in state['summary'].iteritems():
+                self.summary[loc].update(stats)
+        self.details = state['details']
+        self.filter = None
+
+    def getSummary(self):
+        plaindict = {}
+        for k, v in self.summary.iteritems():
+            plaindict[k] = dict(v)
+        return plaindict
+
+    def toJSON(self):
+        return dict(summary=self.getSummary(), details=self.details.toJSON())
+
+    def notify(self, category, file, data):
+        rv = "error"
+        if category in self.stat_cats:
+            # these get called post reporting just for stats
+            # return "error" to forward them to other other_observers
+            self.summary[file.locale][category] += data
+            # keep track of how many strings are in a missing file
+            # we got the {'missingFile': 'error'} from the first pass
+            if category == 'missingInFiles':
+                self.details[file]['strings'] = data
+            return "error"
+        if category in ['missingFile', 'obsoleteFile']:
+            if self.filter is not None:
+                rv = self.filter(file)
+            if rv != "ignore":
+                self.details[file][category] = rv
+            return rv
+        if category in ['missingEntity', 'obsoleteEntity']:
+            if self.filter is not None:
+                rv = self.filter(file, data)
+            if rv == "ignore":
+                return rv
+            v = self.details[file]
+            try:
+                v[category].append(data)
+            except KeyError:
+                v[category] = [data]
+            return rv
+        if category == 'error':
+            try:
+                self.details[file][category].append(data)
+            except KeyError:
+                self.details[file][category] = [data]
+            self.summary[file.locale]['errors'] += 1
+        elif category == 'warning':
+            try:
+                self.details[file][category].append(data)
+            except KeyError:
+                self.details[file][category] = [data]
+            self.summary[file.locale]['warnings'] += 1
+        return rv
+
+    def toExhibit(self):
+        items = []
+        for locale in sorted(self.summary.iterkeys()):
+            summary = self.summary[locale]
+            if locale is not None:
+                item = {'id': 'xxx/' + locale,
+                        'label': locale,
+                        'locale': locale}
+            else:
+                item = {'id': 'xxx',
+                        'label': 'xxx',
+                        'locale': 'xxx'}
+            item['type'] = 'Build'
+            total = sum([summary[k]
+                         for k in ('changed', 'unchanged', 'report', 'missing',
+                                   'missingInFiles')
+                         if k in summary])
+            rate = (('changed' in summary and summary['changed'] * 100)
+                    or 0) / total
+            item.update((k, summary.get(k, 0))
+                        for k in ('changed', 'unchanged'))
+            item.update((k, summary[k])
+                        for k in ('report', 'errors', 'warnings')
+                        if k in summary)
+            item['missing'] = summary.get('missing', 0) + \
+                summary.get('missingInFiles', 0)
+            item['completion'] = rate
+            item['total'] = total
+            result = 'success'
+            if item.get('warnings', 0):
+                result = 'warning'
+            if item.get('errors', 0) or item.get('missing', 0):
+                result = 'failure'
+            item['result'] = result
+            items.append(item)
+        data = {
+            "properties": dict.fromkeys(
+                ("completion", "errors", "warnings", "missing", "report",
+                 "unchanged", "changed", "obsolete"),
+                {"valueType": "number"}),
+            "types": {
+                "Build": {"pluralLabel": "Builds"}
+            }}
+        data['items'] = items
+        return dumps(data, indent=2)
+
+    def serialize(self, type="text"):
+        if type == "exhibit":
+            return self.toExhibit()
+        if type == "json":
+            return dumps(self.toJSON())
+
+        def tostr(t):
+            if t[1] == 'key':
+                return '  ' * t[0] + '/'.join(t[2])
+            o = []
+            indent = '  ' * (t[0] + 1)
+            if 'error' in t[2]:
+                o += [indent + 'ERROR: ' + e for e in t[2]['error']]
+            if 'warning' in t[2]:
+                o += [indent + 'WARNING: ' + e for e in t[2]['warning']]
+            if 'missingEntity' in t[2] or 'obsoleteEntity' in t[2]:
+                missingEntities = ('missingEntity' in t[2] and
+                                   t[2]['missingEntity']) or []
+                obsoleteEntities = ('obsoleteEntity' in t[2] and
+                                    t[2]['obsoleteEntity']) or []
+                entities = missingEntities + obsoleteEntities
+                entities.sort()
+                for entity in entities:
+                    op = '+'
+                    if entity in obsoleteEntities:
+                        op = '-'
+                    o.append(indent + op + entity)
+            elif 'missingFile' in t[2]:
+                o.append(indent + '// add and localize this file')
+            elif 'obsoleteFile' in t[2]:
+                o.append(indent + '// remove this file')
+            return '\n'.join(o)
+
+        out = []
+        for locale, summary in sorted(self.summary.iteritems()):
+            if locale is not None:
+                out.append(locale + ':')
+            out += [k + ': ' + str(v) for k, v in sorted(summary.iteritems())]
+            total = sum([summary[k]
+                         for k in ['changed', 'unchanged', 'report', 'missing',
+                                   'missingInFiles']
+                         if k in summary])
+            rate = 0
+            if total:
+                rate = (('changed' in summary and summary['changed'] * 100)
+                        or 0) / total
+            out.append('%d%% of entries changed' % rate)
+        return '\n'.join(map(tostr, self.details.getContent()) + out)
+
+    def __str__(self):
+        return 'observer'
+
+
+class ContentComparer:
+    keyRE = re.compile('[kK]ey')
+    nl = re.compile('\n', re.M)
+
+    def __init__(self):
+        '''Create a ContentComparer.
+        observer is usually a instance of Observer. The return values
+        of the notify method are used to control the handling of missing
+        entities.
+        '''
+        self.reference = dict()
+        self.observer = Observer()
+        self.other_observers = []
+        self.merge_stage = None
+
+    def add_observer(self, obs):
+        '''Add a non-filtering observer.
+        Results from the notify calls are ignored.
+        '''
+        self.other_observers.append(obs)
+
+    def set_merge_stage(self, merge_stage):
+        self.merge_stage = merge_stage
+
+    def merge(self, ref_entities, ref_map, ref_file, l10n_file, missing,
+              skips, p):
+        outfile = os.path.join(self.merge_stage, l10n_file.module,
+                               l10n_file.file)
+        outdir = os.path.dirname(outfile)
+        if not os.path.isdir(outdir):
+            os.makedirs(outdir)
+        if not p.canMerge:
+            shutil.copyfile(ref_file.fullpath, outfile)
+            print "copied reference to " + outfile
+            return
+        if skips:
+            # skips come in ordered by key name, we need them in file order
+            skips.sort(key=lambda s: s.span[0])
+        trailing = (['\n'] +
+                    [ref_entities[ref_map[key]].all for key in missing] +
+                    [ref_entities[ref_map[skip.key]].all for skip in skips])
+        if skips:
+            # we need to skip a few errornous blocks in the input, copy by hand
+            f = codecs.open(outfile, 'wb', p.encoding)
+            offset = 0
+            for skip in skips:
+                chunk = skip.span
+                f.write(p.contents[offset:chunk[0]])
+                offset = chunk[1]
+            f.write(p.contents[offset:])
+        else:
+            shutil.copyfile(l10n_file.fullpath, outfile)
+            f = codecs.open(outfile, 'ab', p.encoding)
+        print "adding to " + outfile
+
+        def ensureNewline(s):
+            if not s.endswith('\n'):
+                return s + '\n'
+            return s
+
+        f.write(''.join(map(ensureNewline, trailing)))
+        f.close()
+
+    def notify(self, category, file, data):
+        """Check observer for the found data, and if it's
+        not to ignore, notify other_observers.
+        """
+        rv = self.observer.notify(category, file, data)
+        if rv == 'ignore':
+            return rv
+        for obs in self.other_observers:
+            # non-filtering other_observers, ignore results
+            obs.notify(category, file, data)
+        return rv
+
+    def remove(self, obsolete):
+        self.notify('obsoleteFile', obsolete, None)
+        pass
+
+    def compare(self, ref_file, l10n):
+        try:
+            p = parser.getParser(ref_file.file)
+        except UserWarning:
+            # no comparison, XXX report?
+            return
+        if ref_file not in self.reference:
+            # we didn't parse this before
+            try:
+                p.readContents(ref_file.getContents())
+            except Exception, e:
+                self.notify('error', ref_file, str(e))
+                return
+            self.reference[ref_file] = p.parse()
+        ref = self.reference[ref_file]
+        ref_list = ref[1].keys()
+        ref_list.sort()
+        try:
+            p.readContents(l10n.getContents())
+            l10n_entities, l10n_map = p.parse()
+        except Exception, e:
+            self.notify('error', l10n, str(e))
+            return
+        lines = []
+
+        def _getLine(offset):
+            if not lines:
+                lines.append(0)
+                for m in self.nl.finditer(p.contents):
+                    lines.append(m.end())
+            for i in xrange(len(lines), 0, -1):
+                if offset >= lines[i - 1]:
+                    return (i, offset - lines[i - 1])
+            return (1, offset)
+
+        l10n_list = l10n_map.keys()
+        l10n_list.sort()
+        ar = AddRemove()
+        ar.set_left(ref_list)
+        ar.set_right(l10n_list)
+        report = missing = obsolete = changed = unchanged = keys = 0
+        missings = []
+        skips = []
+        checker = getChecker(l10n, reference=ref[0])
+        for action, item_or_pair in ar:
+            if action == 'delete':
+                # missing entity
+                _rv = self.notify('missingEntity', l10n, item_or_pair)
+                if _rv == "ignore":
+                    continue
+                if _rv == "error":
+                    # only add to missing entities for l10n-merge on error,
+                    # not report
+                    missings.append(item_or_pair)
+                    missing += 1
+                else:
+                    # just report
+                    report += 1
+            elif action == 'add':
+                # obsolete entity or junk
+                if isinstance(l10n_entities[l10n_map[item_or_pair]],
+                              parser.Junk):
+                    junk = l10n_entities[l10n_map[item_or_pair]]
+                    params = (junk.val,) + junk.span
+                    self.notify('error', l10n,
+                                'Unparsed content "%s" at %d-%d' % params)
+                elif self.notify('obsoleteEntity', l10n,
+                                 item_or_pair) != 'ignore':
+                    obsolete += 1
+            else:
+                # entity found in both ref and l10n, check for changed
+                entity = item_or_pair[0]
+                refent = ref[0][ref[1][entity]]
+                l10nent = l10n_entities[l10n_map[entity]]
+                if self.keyRE.search(entity):
+                    keys += 1
+                else:
+                    if refent.val == l10nent.val:
+                        self.doUnchanged(l10nent)
+                        unchanged += 1
+                    else:
+                        self.doChanged(ref_file, refent, l10nent)
+                        changed += 1
+                        # run checks:
+                if checker:
+                    for tp, pos, msg, cat in checker.check(refent, l10nent):
+                        # compute real src position, if first line,
+                        # col needs adjustment
+                        _l, _offset = _getLine(l10nent.val_span[0])
+                        if isinstance(pos, tuple):
+                            # line, column
+                            if pos[0] == 1:
+                                col = pos[1] + _offset
+                            else:
+                                col = pos[1]
+                            _l += pos[0] - 1
+                        else:
+                            _l, col = _getLine(l10nent.val_span[0] + pos)
+                            # skip error entities when merging
+                        if tp == 'error' and self.merge_stage is not None:
+                            skips.append(l10nent)
+                        self.notify(tp, l10n,
+                                    u"%s at line %d, column %d for %s" %
+                                    (msg, _l, col, refent.key))
+                pass
+        if missing:
+            self.notify('missing', l10n, missing)
+        if self.merge_stage is not None and (missings or skips):
+            self.merge(ref[0], ref[1], ref_file, l10n, missings, skips, p)
+        if report:
+            self.notify('report', l10n, report)
+        if obsolete:
+            self.notify('obsolete', l10n, obsolete)
+        if changed:
+            self.notify('changed', l10n, changed)
+        if unchanged:
+            self.notify('unchanged', l10n, unchanged)
+        if keys:
+            self.notify('keys', l10n, keys)
+        pass
+
+    def add(self, orig, missing):
+        if self.notify('missingFile', missing, None) == "ignore":
+            # filter said that we don't need this file, don't count it
+            return
+        f = orig
+        try:
+            p = parser.getParser(f.file)
+        except UserWarning:
+            return
+        try:
+            p.readContents(f.getContents())
+            entities, map = p.parse()
+        except Exception, e:
+            self.notify('error', f, str(e))
+            return
+        self.notify('missingInFiles', missing, len(map))
+
+    def doUnchanged(self, entity):
+        # overload this if needed
+        pass
+
+    def doChanged(self, file, ref_entity, l10n_entity):
+        # overload this if needed
+        pass
+
+
+def compareApp(app, other_observer=None, merge_stage=None, clobber=False):
+    '''Compare locales set in app.
+
+    Optional arguments are:
+    - other_observer. A object implementing
+        notify(category, _file, data)
+      The return values of that callback are ignored.
+    - merge_stage. A directory to be used for staging the output of
+      l10n-merge.
+    - clobber. Clobber the module subdirectories of the merge dir as we go.
+      Use wisely, as it might cause data loss.
+    '''
+    comparer = ContentComparer()
+    if other_observer is not None:
+        comparer.add_observer(other_observer)
+    comparer.observer.filter = app.filter
+    for module, reference, locales in app:
+        dir_comp = DirectoryCompare(reference)
+        dir_comp.setWatcher(comparer)
+        for _, localization in locales:
+            if merge_stage is not None:
+                locale_merge = merge_stage.format(ab_CD=localization.locale)
+                comparer.set_merge_stage(locale_merge)
+                if clobber:
+                    # if clobber on, remove the stage for the module if it exists
+                    clobberdir = os.path.join(locale_merge, module)
+                    if os.path.exists(clobberdir):
+                        shutil.rmtree(clobberdir)
+                        print "clobbered " + clobberdir
+            dir_comp.compareWith(localization)
+    return comparer.observer
+
+
+def compareDirs(reference, locale, other_observer=None, merge_stage=None):
+    '''Compare reference and locale dir.
+
+    Optional arguments are:
+    - other_observer. A object implementing
+        notify(category, _file, data)
+      The return values of that callback are ignored.
+    '''
+    comparer = ContentComparer()
+    if other_observer is not None:
+        comparer.add_observer(other_observer)
+    comparer.set_merge_stage(merge_stage)
+    dir_comp = DirectoryCompare(paths.EnumerateDir(reference))
+    dir_comp.setWatcher(comparer)
+    dir_comp.compareWith(paths.EnumerateDir(locale))
+    return comparer.observer
new file mode 100644
--- /dev/null
+++ b/python/compare-locales/compare_locales/parser.py
@@ -0,0 +1,521 @@
+# 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/.
+
+import re
+import codecs
+import logging
+from HTMLParser import HTMLParser
+
+__constructors = []
+
+
+class Entity(object):
+    '''
+    Abstraction layer for a localizable entity.
+    Currently supported are grammars of the form:
+
+    1: pre white space
+    2: pre comments
+    3: entity definition
+    4: entity key (name)
+    5: entity value
+    6: post comment (and white space) in the same line (dtd only)
+                                                 <--[1]
+    <!-- pre comments -->                        <--[2]
+    <!ENTITY key "value"> <!-- comment -->
+
+    <-------[3]---------><------[6]------>
+    '''
+    def __init__(self, contents, pp,
+                 span, pre_ws_span, pre_comment_span, def_span,
+                 key_span, val_span, post_span):
+        self.contents = contents
+        self.span = span
+        self.pre_ws_span = pre_ws_span
+        self.pre_comment_span = pre_comment_span
+        self.def_span = def_span
+        self.key_span = key_span
+        self.val_span = val_span
+        self.post_span = post_span
+        self.pp = pp
+        pass
+
+    # getter helpers
+
+    def get_all(self):
+        return self.contents[self.span[0]:self.span[1]]
+
+    def get_pre_ws(self):
+        return self.contents[self.pre_ws_span[0]:self.pre_ws_span[1]]
+
+    def get_pre_comment(self):
+        return self.contents[self.pre_comment_span[0]:
+                             self.pre_comment_span[1]]
+
+    def get_def(self):
+        return self.contents[self.def_span[0]:self.def_span[1]]
+
+    def get_key(self):
+        return self.contents[self.key_span[0]:self.key_span[1]]
+
+    def get_val(self):
+        return self.pp(self.contents[self.val_span[0]:self.val_span[1]])
+
+    def get_raw_val(self):
+        return self.contents[self.val_span[0]:self.val_span[1]]
+
+    def get_post(self):
+        return self.contents[self.post_span[0]:self.post_span[1]]
+
+    # getters
+
+    all = property(get_all)
+    pre_ws = property(get_pre_ws)
+    pre_comment = property(get_pre_comment)
+    definition = property(get_def)
+    key = property(get_key)
+    val = property(get_val)
+    raw_val = property(get_raw_val)
+    post = property(get_post)
+
+    def __repr__(self):
+        return self.key
+
+
+class Junk(object):
+    '''
+    An almost-Entity, representing junk data that we didn't parse.
+    This way, we can signal bad content as stuff we don't understand.
+    And the either fix that, or report real bugs in localizations.
+    '''
+    junkid = 0
+
+    def __init__(self, contents, span):
+        self.contents = contents
+        self.span = span
+        self.pre_ws = self.pre_comment = self.definition = self.post = ''
+        self.__class__.junkid += 1
+        self.key = '_junk_%d_%d-%d' % (self.__class__.junkid, span[0], span[1])
+
+    # getter helpers
+    def get_all(self):
+        return self.contents[self.span[0]:self.span[1]]
+
+    # getters
+    all = property(get_all)
+    val = property(get_all)
+
+    def __repr__(self):
+        return self.key
+
+
+class Parser:
+    canMerge = True
+
+    def __init__(self):
+        if not hasattr(self, 'encoding'):
+            self.encoding = 'utf-8'
+        pass
+
+    def readFile(self, file):
+        f = codecs.open(file, 'r', self.encoding)
+        try:
+            self.contents = f.read()
+        except UnicodeDecodeError, e:
+            (logging.getLogger('locales')
+                    .error("Can't read file: " + file + '; ' + str(e)))
+            self.contents = u''
+        f.close()
+
+    def readContents(self, contents):
+        (self.contents, length) = codecs.getdecoder(self.encoding)(contents)
+
+    def parse(self):
+        l = []
+        m = {}
+        for e in self:
+            m[e.key] = len(l)
+            l.append(e)
+        return (l, m)
+
+    def postProcessValue(self, val):
+        return val
+
+    def __iter__(self):
+        contents = self.contents
+        offset = 0
+        self.header, offset = self.getHeader(contents, offset)
+        self.footer = ''
+        entity, offset = self.getEntity(contents, offset)
+        while entity:
+            yield entity
+            entity, offset = self.getEntity(contents, offset)
+        f = self.reFooter.match(contents, offset)
+        if f:
+            self.footer = f.group()
+            offset = f.end()
+        if len(contents) > offset:
+            yield Junk(contents, (offset, len(contents)))
+        pass
+
+    def getHeader(self, contents, offset):
+        header = ''
+        h = self.reHeader.match(contents)
+        if h:
+            header = h.group()
+            offset = h.end()
+        return (header, offset)
+
+    def getEntity(self, contents, offset):
+        m = self.reKey.match(contents, offset)
+        if m:
+            offset = m.end()
+            entity = self.createEntity(contents, m)
+            return (entity, offset)
+        # first check if footer has a non-empy match,
+        # 'cause then we don't find junk
+        m = self.reFooter.match(contents, offset)
+        if m and m.end() > offset:
+            return (None, offset)
+        m = self.reKey.search(contents, offset)
+        if m:
+            # we didn't match, but search, so there's junk between offset
+            # and start. We'll match() on the next turn
+            junkend = m.start()
+            return (Junk(contents, (offset, junkend)), junkend)
+        return (None, offset)
+
+    def createEntity(self, contents, m):
+        return Entity(contents, self.postProcessValue,
+                      *[m.span(i) for i in xrange(7)])
+
+
+def getParser(path):
+    for item in __constructors:
+        if re.search(item[0], path):
+            return item[1]
+    raise UserWarning("Cannot find Parser")
+
+
+# Subgroups of the match will:
+# 1: pre white space
+# 2: pre comments
+# 3: entity definition
+# 4: entity key (name)
+# 5: entity value
+# 6: post comment (and white space) in the same line (dtd only)
+#                                            <--[1]
+# <!-- pre comments -->                      <--[2]
+# <!ENTITY key "value"> <!-- comment -->
+#
+# <-------[3]---------><------[6]------>
+
+
+class DTDParser(Parser):
+    # http://www.w3.org/TR/2006/REC-xml11-20060816/#NT-NameStartChar
+    # ":" | [A-Z] | "_" | [a-z] |
+    # [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF]
+    # | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] |
+    # [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] |
+    # [#x10000-#xEFFFF]
+    CharMinusDash = u'\x09\x0A\x0D\u0020-\u002C\u002E-\uD7FF\uE000-\uFFFD'
+    XmlComment = '<!--(?:-?[%s])*?-->' % CharMinusDash
+    NameStartChar = u':A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF' + \
+        u'\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F' + \
+        u'\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD'
+    # + \U00010000-\U000EFFFF seems to be unsupported in python
+
+    # NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 |
+    #     [#x0300-#x036F] | [#x203F-#x2040]
+    NameChar = NameStartChar + ur'\-\.0-9' + u'\xB7\u0300-\u036F\u203F-\u2040'
+    Name = '[' + NameStartChar + '][' + NameChar + ']*'
+    reKey = re.compile('(?:(?P<pre>\s*)(?P<precomment>(?:' + XmlComment +
+                       '\s*)*)(?P<entity><!ENTITY\s+(?P<key>' + Name +
+                       ')\s+(?P<val>\"[^\"]*\"|\'[^\']*\'?)\s*>)'
+                       '(?P<post>[ \t]*(?:' + XmlComment + '\s*)*\n?)?)',
+                       re.DOTALL)
+    # add BOM to DTDs, details in bug 435002
+    reHeader = re.compile(u'^\ufeff?'
+                          u'(\s*<!--.*(http://mozilla.org/MPL/2.0/|'
+                          u'LICENSE BLOCK)([^-]+-)*[^-]+-->)?', re.S)
+    reFooter = re.compile('\s*(<!--([^-]+-)*[^-]+-->\s*)*$')
+    rePE = re.compile('(?:(\s*)((?:' + XmlComment + '\s*)*)'
+                      '(<!ENTITY\s+%\s+(' + Name +
+                      ')\s+SYSTEM\s+(\"[^\"]*\"|\'[^\']*\')\s*>\s*%' + Name +
+                      ';)([ \t]*(?:' + XmlComment + '\s*)*\n?)?)')
+
+    def getEntity(self, contents, offset):
+        '''
+        Overload Parser.getEntity to special-case ParsedEntities.
+        Just check for a parsed entity if that method claims junk.
+
+        <!ENTITY % foo SYSTEM "url">
+        %foo;
+        '''
+        entity, inneroffset = Parser.getEntity(self, contents, offset)
+        if (entity and isinstance(entity, Junk)) or entity is None:
+            m = self.rePE.match(contents, offset)
+            if m:
+                inneroffset = m.end()
+                entity = Entity(contents, self.postProcessValue,
+                                *[m.span(i) for i in xrange(7)])
+        return (entity, inneroffset)
+
+    def createEntity(self, contents, m):
+        valspan = m.span('val')
+        valspan = (valspan[0]+1, valspan[1]-1)
+        return Entity(contents, self.postProcessValue, m.span(),
+                      m.span('pre'), m.span('precomment'),
+                      m.span('entity'), m.span('key'), valspan,
+                      m.span('post'))
+
+
+class PropertiesParser(Parser):
+    escape = re.compile(r'\\((?P<uni>u[0-9a-fA-F]{1,4})|'
+                        '(?P<nl>\n\s*)|(?P<single>.))', re.M)
+    known_escapes = {'n': '\n', 'r': '\r', 't': '\t', '\\': '\\'}
+
+    def __init__(self):
+        self.reKey = re.compile('^(\s*)'
+                                '((?:[#!].*?\n\s*)*)'
+                                '([^#!\s\n][^=:\n]*?)\s*[:=][ \t]*', re.M)
+        self.reHeader = re.compile('^\s*([#!].*\s*)+')
+        self.reFooter = re.compile('\s*([#!].*\s*)*$')
+        self._escapedEnd = re.compile(r'\\+$')
+        self._trailingWS = re.compile(r'[ \t]*$')
+        Parser.__init__(self)
+
+    def getHeader(self, contents, offset):
+        header = ''
+        h = self.reHeader.match(contents, offset)
+        if h:
+            candidate = h.group()
+            if 'http://mozilla.org/MPL/2.0/' in candidate or \
+                    'LICENSE BLOCK' in candidate:
+                header = candidate
+                offset = h.end()
+        return (header, offset)
+
+    def getEntity(self, contents, offset):
+        # overwritten to parse values line by line
+        m = self.reKey.match(contents, offset)
+        if m:
+            offset = m.end()
+            while True:
+                endval = nextline = contents.find('\n', offset)
+                if nextline == -1:
+                    endval = offset = len(contents)
+                    break
+                # is newline escaped?
+                _e = self._escapedEnd.search(contents, offset, nextline)
+                offset = nextline + 1
+                if _e is None:
+                    break
+                # backslashes at end of line, if 2*n, not escaped
+                if len(_e.group()) % 2 == 0:
+                    break
+            # strip trailing whitespace
+            ws = self._trailingWS.search(contents, m.end(), offset)
+            if ws:
+                endval -= ws.end() - ws.start()
+            entity = Entity(contents, self.postProcessValue,
+                            (m.start(), offset),   # full span
+                            m.span(1),  # leading whitespan
+                            m.span(2),  # leading comment span
+                            (m.start(3), offset),   # entity def span
+                            m.span(3),   # key span
+                            (m.end(), endval),   # value span
+                            (offset, offset))  # post comment span, empty
+            return (entity, offset)
+        m = self.reKey.search(contents, offset)
+        if m:
+            # we didn't match, but search, so there's junk between offset
+            # and start. We'll match() on the next turn
+            junkend = m.start()
+            return (Junk(contents, (offset, junkend)), junkend)
+        return (None, offset)
+
+    def postProcessValue(self, val):
+
+        def unescape(m):
+            found = m.groupdict()
+            if found['uni']:
+                return unichr(int(found['uni'][1:], 16))
+            if found['nl']:
+                return ''
+            return self.known_escapes.get(found['single'], found['single'])
+        val = self.escape.sub(unescape, val)
+        return val
+
+
+class DefinesParser(Parser):
+    # can't merge, #unfilter needs to be the last item, which we don't support
+    canMerge = False
+
+    def __init__(self):
+        self.reKey = re.compile('^(\s*)((?:^#(?!define\s).*\s*)*)'
+                                '(#define[ \t]+(\w+)[ \t]+(.*?))([ \t]*$\n?)',
+                                re.M)
+        self.reHeader = re.compile('^\s*(#(?!define\s).*\s*)*')
+        self.reFooter = re.compile('\s*(#(?!define\s).*\s*)*$', re.M)
+        Parser.__init__(self)
+
+
+class IniParser(Parser):
+    '''
+    Parse files of the form:
+    # initial comment
+    [cat]
+    whitespace*
+    #comment
+    string=value
+    ...
+    '''
+    def __init__(self):
+        self.reHeader = re.compile('^((?:\s*|[;#].*)\n)*\[.+?\]\n', re.M)
+        self.reKey = re.compile('(\s*)((?:[;#].*\n\s*)*)((.+?)=(.*))(\n?)')
+        self.reFooter = re.compile('\s*')
+        Parser.__init__(self)
+
+
+DECL, COMMENT, START, END, CONTENT = range(5)
+
+
+class BookmarksParserInner(HTMLParser):
+
+    class Token(object):
+        _type = None
+        content = ''
+
+        def __str__(self):
+            return self.content
+
+    class DeclToken(Token):
+        _type = DECL
+
+        def __init__(self, decl):
+            self.content = decl
+            pass
+
+        def __str__(self):
+            return '<!%s>' % self.content
+        pass
+
+    class CommentToken(Token):
+        _type = COMMENT
+
+        def __init__(self, comment):
+            self.content = comment
+            pass
+
+        def __str__(self):
+            return '<!--%s-->' % self.content
+        pass
+
+    class StartToken(Token):
+        _type = START
+
+        def __init__(self, tag, attrs, content):
+            self.tag = tag
+            self.attrs = dict(attrs)
+            self.content = content
+            pass
+        pass
+
+    class EndToken(Token):
+        _type = END
+
+        def __init__(self, tag):
+            self.tag = tag
+            pass
+
+        def __str__(self):
+            return '</%s>' % self.tag.upper()
+        pass
+
+    class ContentToken(Token):
+        _type = CONTENT
+
+        def __init__(self, content):
+            self.content = content
+            pass
+        pass
+
+    def __init__(self):
+        HTMLParser.__init__(self)
+        self.tokens = []
+
+    def parse(self, contents):
+        self.tokens = []
+        self.feed(contents)
+        self.close()
+        return self.tokens
+
+    # Called when we hit an end DL tag to reset the folder selections
+    def handle_decl(self, decl):
+        self.tokens.append(self.DeclToken(decl))
+
+    # Called when we hit an end DL tag to reset the folder selections
+    def handle_comment(self, comment):
+        self.tokens.append(self.CommentToken(comment))
+
+    def handle_starttag(self, tag, attrs):
+        self.tokens.append(self.StartToken(tag, attrs,
+                                           self.get_starttag_text()))
+
+    # Called when text data is encountered
+    def handle_data(self, data):
+        if self.tokens[-1]._type == CONTENT:
+            self.tokens[-1].content += data
+        else:
+            self.tokens.append(self.ContentToken(data))
+
+    def handle_charref(self, data):
+        self.handle_data('&#%s;' % data)
+
+    def handle_entityref(self, data):
+        self.handle_data('&%s;' % data)
+
+    # Called when we hit an end DL tag to reset the folder selections
+    def handle_endtag(self, tag):
+        self.tokens.append(self.EndToken(tag))
+
+
+class BookmarksParser(Parser):
+    canMerge = False
+
+    class BMEntity(object):
+        def __init__(self, key, val):
+            self.key = key
+            self.val = val
+
+    def __iter__(self):
+        p = BookmarksParserInner()
+        tks = p.parse(self.contents)
+        i = 0
+        k = []
+        for i in xrange(len(tks)):
+            t = tks[i]
+            if t._type == START:
+                k.append(t.tag)
+                keys = t.attrs.keys()
+                keys.sort()
+                for attrname in keys:
+                    yield self.BMEntity('.'.join(k) + '.@' + attrname,
+                                        t.attrs[attrname])
+                if i + 1 < len(tks) and tks[i+1]._type == CONTENT:
+                    i += 1
+                    t = tks[i]
+                    v = t.content.strip()
+                    if v:
+                        yield self.BMEntity('.'.join(k), v)
+            elif t._type == END:
+                k.pop()
+
+
+__constructors = [('\\.dtd$', DTDParser()),
+                  ('\\.properties$', PropertiesParser()),
+                  ('\\.ini$', IniParser()),
+                  ('\\.inc$', DefinesParser()),
+                  ('bookmarks\\.html$', BookmarksParser())]
new file mode 100644
--- /dev/null
+++ b/python/compare-locales/compare_locales/paths.py
@@ -0,0 +1,398 @@
+# 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/.
+
+import os.path
+import os
+from ConfigParser import ConfigParser, NoSectionError, NoOptionError
+from urlparse import urlparse, urljoin
+from urllib import pathname2url, url2pathname
+from urllib2 import urlopen
+from collections import defaultdict
+from compare_locales import util
+
+
+class L10nConfigParser(object):
+    '''Helper class to gather application information from ini files.
+
+    This class is working on synchronous open to read files or web data.
+    Subclass this and overwrite loadConfigs and addChild if you need async.
+    '''
+    def __init__(self, inipath, **kwargs):
+        """Constructor for L10nConfigParsers
+
+        inipath -- l10n.ini path
+        Optional keyword arguments are fowarded to the inner ConfigParser as
+        defaults.
+        """
+        if os.path.isabs(inipath):
+            self.inipath = 'file:%s' % pathname2url(inipath)
+        else:
+            pwdurl = 'file:%s/' % pathname2url(os.getcwd())
+            self.inipath = urljoin(pwdurl, inipath)
+        # l10n.ini files can import other l10n.ini files, store the
+        # corresponding L10nConfigParsers
+        self.children = []
+        # we really only care about the l10n directories described in l10n.ini
+        self.dirs = []
+        # optional defaults to be passed to the inner ConfigParser (unused?)
+        self.defaults = kwargs
+
+    def getDepth(self, cp):
+        '''Get the depth for the comparison from the parsed l10n.ini.
+
+        Overloadable to get the source depth for fennec and friends.
+        '''
+        try:
+            depth = cp.get('general', 'depth')
+        except:
+            depth = '.'
+        return depth
+
+    def getFilters(self):
+        '''Get the test functions from this ConfigParser and all children.
+
+        Only works with synchronous loads, used by compare-locales, which
+        is local anyway.
+        '''
+        filterurl = urljoin(self.inipath, 'filter.py')
+        try:
+            l = {}
+            execfile(url2pathname(urlparse(filterurl).path), {}, l)
+            if 'test' in l and callable(l['test']):
+                filters = [l['test']]
+            else:
+                filters = []
+        except:
+            filters = []
+
+        for c in self.children:
+            filters += c.getFilters()
+
+        return filters
+
+    def loadConfigs(self):
+        """Entry point to load the l10n.ini file this Parser refers to.
+
+        This implementation uses synchronous loads, subclasses might overload
+        this behaviour. If you do, make sure to pass a file-like object
+        to onLoadConfig.
+        """
+        self.onLoadConfig(urlopen(self.inipath))
+
+    def onLoadConfig(self, inifile):
+        """Parse a file-like object for the loaded l10n.ini file."""
+        cp = ConfigParser(self.defaults)
+        cp.readfp(inifile)
+        depth = self.getDepth(cp)
+        self.baseurl = urljoin(self.inipath, depth)
+        # create child loaders for any other l10n.ini files to be included
+        try:
+            for title, path in cp.items('includes'):
+                # skip default items
+                if title in self.defaults:
+                    continue
+                # add child config parser
+                self.addChild(title, path, cp)
+        except NoSectionError:
+            pass
+        # try to load the "dirs" defined in the "compare" section
+        try:
+            self.dirs.extend(cp.get('compare', 'dirs').split())
+        except (NoOptionError, NoSectionError):
+            pass
+        # try getting a top level compare dir, as used for fennec
+        try:
+            self.tld = cp.get('compare', 'tld')
+            # remove tld from comparison dirs
+            if self.tld in self.dirs:
+                self.dirs.remove(self.tld)
+        except (NoOptionError, NoSectionError):
+            self.tld = None
+        # try to set "all_path" and "all_url"
+        try:
+            self.all_path = cp.get('general', 'all')
+            self.all_url = urljoin(self.baseurl, self.all_path)
+        except (NoOptionError, NoSectionError):
+            self.all_path = None
+            self.all_url = None
+        return cp
+
+    def addChild(self, title, path, orig_cp):
+        """Create a child L10nConfigParser and load it.
+
+        title -- indicates the module's name
+        path -- indicates the path to the module's l10n.ini file
+        orig_cp -- the configuration parser of this l10n.ini
+        """
+        cp = L10nConfigParser(urljoin(self.baseurl, path), **self.defaults)
+        cp.loadConfigs()
+        self.children.append(cp)
+
+    def getTLDPathsTuple(self, basepath):
+        """Given the basepath, return the path fragments to be used for
+        self.tld. For build runs, this is (basepath, self.tld), for
+        source runs, just (basepath,).
+
+        @see overwritten method in SourceTreeConfigParser.
+        """
+        return (basepath, self.tld)
+
+    def dirsIter(self):
+        """Iterate over all dirs and our base path for this l10n.ini"""
+        url = urlparse(self.baseurl)
+        basepath = url2pathname(url.path)
+        if self.tld is not None:
+            yield self.tld, self.getTLDPathsTuple(basepath)
+        for dir in self.dirs:
+            yield dir, (basepath, dir)
+
+    def directories(self):
+        """Iterate over all dirs and base paths for this l10n.ini as well
+        as the included ones.
+        """
+        for t in self.dirsIter():
+            yield t
+        for child in self.children:
+            for t in child.directories():
+                yield t
+
+    def allLocales(self):
+        """Return a list of all the locales of this project"""
+        return util.parseLocales(urlopen(self.all_url).read())
+
+
+class SourceTreeConfigParser(L10nConfigParser):
+    '''Subclassing L10nConfigParser to work with just the repos
+    checked out next to each other instead of intermingled like
+    we do for real builds.
+    '''
+
+    def __init__(self, inipath, basepath):
+        '''Add additional arguments basepath.
+
+        basepath is used to resolve local paths via branchnames.
+        '''
+        L10nConfigParser.__init__(self, inipath)
+        self.basepath = basepath
+        self.tld = None
+
+    def getDepth(self, cp):
+        '''Get the depth for the comparison from the parsed l10n.ini.
+
+        Overloaded to get the source depth for fennec and friends.
+        '''
+        try:
+            depth = cp.get('general', 'source-depth')
+        except:
+            try:
+                depth = cp.get('general', 'depth')
+            except:
+                depth = '.'
+        return depth
+
+    def addChild(self, title, path, orig_cp):
+        # check if there's a section with details for this include
+        # we might have to check a different repo, or even VCS
+        # for example, projects like "mail" indicate in
+        # an "include_" section where to find the l10n.ini for "toolkit"
+        details = 'include_' + title
+        if orig_cp.has_section(details):
+            branch = orig_cp.get(details, 'mozilla')
+            inipath = orig_cp.get(details, 'l10n.ini')
+            path = self.basepath + '/' + branch + '/' + inipath
+        else:
+            path = urljoin(self.baseurl, path)
+        cp = SourceTreeConfigParser(path, self.basepath, **self.defaults)
+        cp.loadConfigs()
+        self.children.append(cp)
+
+    def getTLDPathsTuple(self, basepath):
+        """Overwrite L10nConfigParser's getTLDPathsTuple to just return
+        the basepath.
+        """
+        return (basepath, )
+
+
+class File(object):
+
+    def __init__(self, fullpath, file, module=None, locale=None):
+        self.fullpath = fullpath
+        self.file = file
+        self.module = module
+        self.locale = locale
+        pass
+
+    def getContents(self):
+        # open with universal line ending support and read
+        return open(self.fullpath, 'rU').read()
+
+    def __hash__(self):
+        f = self.file
+        if self.module:
+            f = self.module + '/' + f
+        return hash(f)
+
+    def __str__(self):
+        return self.fullpath
+
+    def __cmp__(self, other):
+        if not isinstance(other, File):
+            raise NotImplementedError
+        rv = cmp(self.module, other.module)
+        if rv != 0:
+            return rv
+        return cmp(self.file, other.file)
+
+
+class EnumerateDir(object):
+    ignore_dirs = ['CVS', '.svn', '.hg', '.git']
+
+    def __init__(self, basepath, module='', locale=None, ignore_subdirs=[]):
+        self.basepath = basepath
+        self.module = module
+        self.locale = locale
+        self.ignore_subdirs = ignore_subdirs
+        pass
+
+    def cloneFile(self, other):
+        '''
+        Return a File object that this enumerator would return, if it had it.
+        '''
+        return File(os.path.join(self.basepath, other.file), other.file,
+                    self.module, self.locale)
+
+    def __iter__(self):
+        # our local dirs are given as a tuple of path segments, starting off
+        # with an empty sequence for the basepath.
+        dirs = [()]
+        while dirs:
+            dir = dirs.pop(0)
+            fulldir = os.path.join(self.basepath, *dir)
+            try:
+                entries = os.listdir(fulldir)
+            except OSError:
+                # we probably just started off in a non-existing dir, ignore
+                continue
+            entries.sort()
+            for entry in entries:
+                leaf = os.path.join(fulldir, entry)
+                if os.path.isdir(leaf):
+                    if entry not in self.ignore_dirs and \
+                        leaf not in [os.path.join(self.basepath, d)
+                                     for d in self.ignore_subdirs]:
+                        dirs.append(dir + (entry,))
+                    continue
+                yield File(leaf, '/'.join(dir + (entry,)),
+                           self.module, self.locale)
+
+
+class LocalesWrap(object):
+
+    def __init__(self, base, module, locales, ignore_subdirs=[]):
+        self.base = base
+        self.module = module
+        self.locales = locales
+        self.ignore_subdirs = ignore_subdirs
+
+    def __iter__(self):
+        for locale in self.locales:
+            path = os.path.join(self.base, locale, self.module)
+            yield (locale, EnumerateDir(path, self.module, locale,
+                                        self.ignore_subdirs))
+
+
+class EnumerateApp(object):
+    reference = 'en-US'
+
+    def __init__(self, inipath, l10nbase, locales=None):
+        self.setupConfigParser(inipath)
+        self.modules = defaultdict(dict)
+        self.l10nbase = os.path.abspath(l10nbase)
+        self.filters = []
+        drive, tail = os.path.splitdrive(inipath)
+        self.addFilters(*self.config.getFilters())
+        self.locales = locales or self.config.allLocales()
+        self.locales.sort()
+
+    def setupConfigParser(self, inipath):
+        self.config = L10nConfigParser(inipath)
+        self.config.loadConfigs()
+
+    def addFilters(self, *args):
+        self.filters += args
+
+    value_map = {None: None, 'error': 0, 'ignore': 1, 'report': 2}
+
+    def filter(self, l10n_file, entity=None):
+        '''Go through all added filters, and,
+        - map "error" -> 0, "ignore" -> 1, "report" -> 2
+        - if filter.test returns a bool, map that to
+            False -> "ignore" (1), True -> "error" (0)
+        - take the max of all reported
+        '''
+        rv = 0
+        for f in reversed(self.filters):
+            try:
+                _r = f(l10n_file.module, l10n_file.file, entity)
+            except:
+                # XXX error handling
+                continue
+            if isinstance(_r, bool):
+                _r = [1, 0][_r]
+            else:
+                # map string return value to int, default to 'error',
+                # None is None
+                _r = self.value_map.get(_r, 0)
+            if _r is not None:
+                rv = max(rv, _r)
+        return ['error', 'ignore', 'report'][rv]
+
+    def __iter__(self):
+        '''
+        Iterate over all modules, return en-US directory enumerator, and an
+        iterator over all locales in each iteration. Per locale, the locale
+        code and an directory enumerator will be given.
+        '''
+        dirmap = dict(self.config.directories())
+        mods = dirmap.keys()
+        mods.sort()
+        for mod in mods:
+            if self.reference == 'en-US':
+                base = os.path.join(*(dirmap[mod] + ('locales', 'en-US')))
+            else:
+                base = os.path.join(self.l10nbase, self.reference, mod)
+            yield (mod, EnumerateDir(base, mod, self.reference),
+                   LocalesWrap(self.l10nbase, mod, self.locales,
+                   [m[len(mod)+1:] for m in mods if m.startswith(mod+'/')]))
+
+
+class EnumerateSourceTreeApp(EnumerateApp):
+    '''Subclass EnumerateApp to work on side-by-side checked out
+    repos, and to no pay attention to how the source would actually
+    be checked out for building.
+
+    It's supporting applications like Fennec, too, which have
+    'locales/en-US/...' in their root dir, but claim to be 'mobile'.
+    '''
+
+    def __init__(self, inipath, basepath, l10nbase, locales=None):
+        self.basepath = basepath
+        EnumerateApp.__init__(self, inipath, l10nbase, locales)
+
+    def setupConfigParser(self, inipath):
+        self.config = SourceTreeConfigParser(inipath, self.basepath)
+        self.config.loadConfigs()
+
+
+def get_base_path(mod, loc):
+    'statics for path patterns and conversion'
+    __l10n = 'l10n/%(loc)s/%(mod)s'
+    __en_US = 'mozilla/%(mod)s/locales/en-US'
+    if loc == 'en-US':
+        return __en_US % {'mod': mod}
+    return __l10n % {'mod': mod, 'loc': loc}
+
+
+def get_path(mod, loc, leaf):
+    return get_base_path(mod, loc) + '/' + leaf
new file mode 100644
--- /dev/null
+++ b/python/compare-locales/compare_locales/tests/__init__.py
@@ -0,0 +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/.
+
+'''Mixins for parser tests.
+'''
+
+from itertools import izip_longest
+from pkg_resources import resource_string
+import re
+
+from compare_locales.parser import getParser
+
+
+class ParserTestMixin():
+    '''Utility methods used by the parser tests.
+    '''
+    filename = None
+
+    def setUp(self):
+        '''Create a parser for this test.
+        '''
+        self.parser = getParser(self.filename)
+
+    def tearDown(self):
+        'tear down this test'
+        del self.parser
+
+    def resource(self, name):
+        testcontent = resource_string(__name__, 'data/' + name)
+        # fake universal line endings
+        testcontent = re.sub('\r\n?', lambda m: '\n', testcontent)
+        return testcontent
+
+    def _test(self, content, refs):
+        '''Helper to test the parser.
+        Compares the result of parsing content with the given list
+        of reference keys and values.
+        '''
+        self.parser.readContents(content)
+        entities = [entity for entity in self.parser]
+        for entity, ref in izip_longest(entities, refs):
+            self.assertTrue(entity, 'excess reference entitiy')
+            self.assertTrue(ref, 'excess parsed entity')
+            self.assertEqual(entity.val, ref[1])
+            if ref[0].startswith('_junk'):
+                self.assertTrue(re.match(ref[0], entity.key))
+            else:
+                self.assertEqual(entity.key, ref[0])
new file mode 100644
--- /dev/null
+++ b/python/compare-locales/compare_locales/tests/data/bug121341.properties
@@ -0,0 +1,68 @@