Merge m-c to b-i
authorPhil Ringnalda <philringnalda@gmail.com>
Sun, 23 Mar 2014 08:42:27 -0700
changeset 174979 c60458764950d7440e5b044dfd8505b3fc8ed744
parent 174978 9adb5fb76a4529814c04d648a342c42f9b05ece5 (current diff)
parent 174969 201feeaf31f3c7d44debaf40f9cb65edc56f14a2 (diff)
child 174980 d474a77ddd1c56d4dcc7a52c5f8fc5fcd59e947e
push id41399
push userphilringnalda@gmail.com
push dateMon, 24 Mar 2014 00:17:26 +0000
treeherdermozilla-inbound@fa098f9fe89c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone31.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 b-i
browser/components/feeds/src/FeedWriterEnabled.h
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6870,22 +6870,28 @@ let gRemoteTabsUI = {
  * @param aOpenParams
  *        If switching to this URI results in us opening a tab, aOpenParams
  *        will be the parameter object that gets passed to openUILinkIn. Please
  *        see the documentation for openUILinkIn to see what parameters can be
  *        passed via this object.
  * @return True if an existing tab was found, false otherwise
  */
 function switchToTabHavingURI(aURI, aOpenNew, aOpenParams) {
+  // Certain URLs can be switched to irrespective of the source or destination
+  // window being in private browsing mode:
+  const kPrivateBrowsingWhitelist = new Set([
+    "about:customizing",
+  ]);
   // This will switch to the tab in aWindow having aURI, if present.
   function switchIfURIInWindow(aWindow) {
-    // Only switch to the tab if neither the source and desination window are
-    // private and they are not in permanent private borwsing mode
-    if ((PrivateBrowsingUtils.isWindowPrivate(window) ||
-        PrivateBrowsingUtils.isWindowPrivate(aWindow)) &&
+    // Only switch to the tab if neither the source nor the destination window
+    // are private and they are not in permanent private browsing mode
+    if (!kPrivateBrowsingWhitelist.has(aURI.spec) &&
+        (PrivateBrowsingUtils.isWindowPrivate(window) ||
+         PrivateBrowsingUtils.isWindowPrivate(aWindow)) &&
         !PrivateBrowsingUtils.permanentPrivateBrowsing) {
       return false;
     }
 
     let browsers = aWindow.gBrowser.browsers;
     for (let i = 0; i < browsers.length; i++) {
       let browser = browsers[i];
       if (browser.currentURI.equals(aURI)) {
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -765,17 +765,16 @@ const CustomizableWidgets = [{
 
       for (let item of list) {
         let elem = aDocument.createElementNS(kNSXUL, "toolbarbutton");
         elem.setAttribute("label", item.label);
         elem.setAttribute("type", "checkbox");
         elem.section = aSection;
         elem.value = item.value;
         elem.setAttribute("class", "subviewbutton");
-        addShortcut(item, doc, elem);
         containerElem.appendChild(elem);
       }
     },
     updateCurrentCharset: function(aDocument) {
       let content = aDocument.defaultView.content;
       let currentCharset = content && content.document && content.document.characterSet;
       currentCharset = CharsetMenu.foldCharset(currentCharset);
 
deleted file mode 100644
--- a/browser/components/feeds/src/FeedWriterEnabled.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* 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 "js/TypeDecls.h"
-#include "nsGlobalWindow.h"
-#include "nsIPrincipal.h"
-#include "nsIURI.h"
-#include "nsString.h"
-#include "xpcpublic.h"
-
-namespace mozilla {
-
-struct FeedWriterEnabled {
-  static bool IsEnabled(JSContext* cx, JSObject* aGlobal)
-  {
-    // Make sure the global is a window
-    nsGlobalWindow* win = xpc::WindowGlobalOrNull(aGlobal);
-    if (!win) {
-      return false;
-    }
-
-    // Make sure that the principal is about:feeds.
-    nsCOMPtr<nsIPrincipal> principal = win->GetPrincipal();
-    NS_ENSURE_TRUE(principal, false);
-    nsCOMPtr<nsIURI> uri;
-    principal->GetURI(getter_AddRefs(uri));
-    if (!uri) {
-      return false;
-    }
-
-    // First check the scheme to avoid getting long specs in the common case.
-    bool isAbout = false;
-    uri->SchemeIs("about", &isAbout);
-    if (!isAbout) {
-      return false;
-    }
-
-    // Now check the spec itself
-    nsAutoCString spec;
-    uri->GetSpec(spec);
-    return spec.Equals("about:feeds");
-  }
-};
-
-}
--- a/browser/components/feeds/src/moz.build
+++ b/browser/components/feeds/src/moz.build
@@ -3,20 +3,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 SOURCES += [
     'nsFeedSniffer.cpp',
 ]
 
-EXPORTS.mozilla += [
-    'FeedWriterEnabled.h',
-]
-
 EXTRA_COMPONENTS += [
     'BrowserFeeds.manifest',
     'FeedConverter.js',
     'WebContentConverter.js',
 ]
 
 EXTRA_PP_COMPONENTS += [
     'FeedWriter.js',
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -7,17 +7,16 @@ const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource:///modules/SignInToWebsite.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
                                   "resource:///modules/AboutHome.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentClick",
@@ -83,16 +82,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource:///modules/sessionstore/SessionStore.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
                                   "resource:///modules/BrowserUITelemetry.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 
+#ifdef NIGHTLY_BUILD
+XPCOMUtils.defineLazyModuleGetter(this, "SignInToWebsiteUX",
+                                  "resource:///modules/SignInToWebsite.jsm");
+#endif
+
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
 // Seconds of idle before trying to create a bookmarks backup.
 const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 10 * 60;
 // Minimum interval between backups.  We try to not create more than one backup
 // per interval.
 const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
@@ -467,17 +471,21 @@ BrowserGlue.prototype = {
     this._migrateUI();
 
     this._syncSearchEngines();
 
     WebappManager.init();
     PageThumbs.init();
     NewTabUtils.init();
     BrowserNewTabPreloader.init();
-    SignInToWebsiteUX.init();
+#ifdef NIGHTLY_BUILD
+    if (Services.prefs.getBoolPref("dom.identity.enabled")) {
+      SignInToWebsiteUX.init();
+    }
+#endif
     PdfJs.init();
 #ifdef NIGHTLY_BUILD
     ShumwayUtils.init();
 #endif
     webrtcUI.init();
     AboutHome.init();
     SessionStore.init();
     BrowserUITelemetry.init();
@@ -650,17 +658,21 @@ BrowserGlue.prototype = {
       appStartup.trackStartupCrashEnd();
     } catch (e) {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
     BrowserNewTabPreloader.uninit();
     CustomizationTabPreloader.uninit();
     WebappManager.uninit();
-    SignInToWebsiteUX.uninit();
+#ifdef NIGHTLY_BUILD
+    if (Services.prefs.getBoolPref("dom.identity.enabled")) {
+      SignInToWebsiteUX.uninit();
+    }
+#endif
     webrtcUI.uninit();
   },
 
   // All initial windows have opened.
   _onWindowsRestored: function BG__onWindowsRestored() {
     // Show update notification, if needed.
     if (Services.prefs.prefHasUserValue("app.update.postupdate"))
       this._showUpdateNotification();
--- a/browser/components/preferences/sync.xul
+++ b/browser/components/preferences/sync.xul
@@ -181,17 +181,17 @@
         <!-- These panels are for the Firefox Accounts identity provider -->
         <vbox id="fxaDeterminingStatus" align="center">
           <spacer flex="1"/>
           <p>&determiningAcctStatus.label;</p>
           <spacer flex="1"/>
         </vbox>
 
         <vbox id="noFxaAccount">
-          <label value="&welcome.description;"/>
+          <label>&welcome.description;</label>
           <label class="text-link"
                  onclick="gSyncPane.signUp(); return false;"
                  value="&welcome.createAccount.label;"/>
           <label class="text-link"
                  onclick="gSyncPane.signIn(); return false;"
                  value="&welcome.signIn.label;"/>
           <separator/>
           <label class="text-link"
--- a/browser/devtools/netmonitor/test/browser_net_filter-02.js
+++ b/browser/devtools/netmonitor/test/browser_net_filter-02.js
@@ -4,16 +4,19 @@
 /**
  * Test if filtering items in the network table works correctly with new requests.
  */
 
 function test() {
   initNetMonitor(FILTERING_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
+    // It seems that this test may be slow on Ubuntu builds running on ec2.
+    requestLongerTimeout(2);
+
     let { $, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 8).then(() => {
       EventUtils.sendMouseEvent({ type: "mousedown" }, $("#details-pane-toggle"));
 
--- a/browser/locales/en-US/searchplugins/creativecommons.xml
+++ b/browser/locales/en-US/searchplugins/creativecommons.xml
@@ -2,14 +2,14 @@
    - 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/. -->
 
 <SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
 <ShortName>Creative Commons</ShortName>
 <Description>Find photos, movies, music, and text to rip, sample, mash, and share.</Description>
 <InputEncoding>utf-8</InputEncoding>
 <Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAJUSURBVDiNjZO9S1thGMXPvTfJFbnkipNkLLS0ksFg0M0lf4CboNCEgIIg/RiKtEOn0qGWTtbVoBYcIji10I9J0ApWCjp0kRaXdhHjTW4+uGnur4NJ8GOwZ3nf4TnnfZ5z3scAdBGGYdyVdN+yrGHTNNOtVqsVhuG+pO+S3gE/LtV3BIxzPDJNc8FxHGN0dNRKpVIGoJ2dndr+/r5Vr9cl6bmkN0AoSQIEyHXdj5KYnZ3F932uolKpkM/nK5KQ9FmSCZwLOI7zQBLr6+vXiFdRLBaDtsiTTve3YrFYkM/nbyR3MDU1dSKpLumO+vr6Xruui+d5AFSrVVZWVtjY2KDRaABwdHTE4uIie3t7AJTLZaLRaFXSCyUSid1MJgOA53n09/eTTqdJJpPMzc2xurqKbduMj48Tj8fZ3d0FYHBw8FjSezmOU56fnwdgeXkZ27ap1WpUKhWazSZjY2Nks1kASqVSd4zp6eljSX/MtiHdRDpnEATyfb+bkiSVSqXu3TCM8xgHBga+dkY4OzvDdV2GhoZIJBLMzMxQKBSIRqNkMhlisRhbW1sAJJPJn5I+KB6Pv7poou/7rK2tsbm5SRAEXROXlpY4ODgAoFarYdu2J+llN8ZcLvffMeZyud+SGpLuCVBPT89jSRQKhRvJxWKxISmU9JTOT5Rk9Pb2fpHE5OQkJycn14inp6dMTEx4bdM/SbKAy8sk6WEkElmwLCuSSqUYGRmxgHB7e7t+eHgYazabgaRnkt7SeZnr63xbUtYwjGHTNNNhGP4F9iR9a6/zr4v1/wDE1D9XlC4rrAAAAABJRU5ErkJggg==</Image>
-<Url type="text/html" method="GET" template="http://search.creativecommons.org/">
+<Url type="text/html" method="GET" template="http://search.creativecommons.org/" resultdomain="creativecommons.org">
   <Param name="q" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
 <SearchForm>http://search.creativecommons.org/</SearchForm>
 </SearchPlugin>
--- a/browser/locales/en-US/searchplugins/eBay.xml
+++ b/browser/locales/en-US/searchplugins/eBay.xml
@@ -6,14 +6,14 @@
 <ShortName>eBay</ShortName>
 <Description>eBay - Online auctions</Description>
 <InputEncoding>ISO-8859-1</InputEncoding>
 <Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAAAAAB6AQAAJgAAACAgAAAAAAAAQgMAAKABAACJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAAFBSURBVDjLtZPdK0MBGIf3J5Babhx3rinFBWuipaUskX9DYvkopqgV90q5UJpyp0OKrUWM2VrRsS9D0zZKHGaOnW1nj4vtypVtPPe/533r9746QAAOAJXfo5Yzgg44pHrcugon/6Sgo0b+XuAOZ2iZiVQmyPoDpIwmUkYTzqM7GsdDdC7F6Lbf8pzOkfWOouzqeZem2b+2AqAV8zjD8yVBqqcf2b7C66yNiMGMfixIQSvi8Mp0LEbR5ADq1QSKWM+Gx0RC9nOZ2GLzwlIWdPWiuNzk4w/EpThNkyEAXKEP2ud8KGId2sspilhPMrmNwzfCuqePr/xbSfC5I/I0MMSj2YJ3z49gDdO2cEOrLUowJpE9G0QRG1ClKbR0EIdvmOPYcnUtnN+vsnZiQC1k/qnGagQ1n3LNzySUJZVskitnmr8BlQG7T2hvgxsAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAAMJSURBVFjD7ZddSFNhGMeHXXQTZFFCWfR1pRhUECQlBdWVToo+6KYu1KigtDASG5qUfZgFZvahEDosECPDktKZS1FL+1DRnEvdUptjug91X2dnZzv/3vO6OZbWnR4v9sADL+fs7P97/s/znu2VAJD4UkpSSdKG+QubTyPBr+sXz8XCR64fIAHihVTis0SsUAoAVhEBrBKIHCGAEMB/ARi3F5LkbpS2WMRzYEEBXC2tsD6T03R9agsCGLNyqPw6CXmrBT06JvhbPHZwmkdwtR0B138PPKOHgzXD5jLAy3tmibo4K9weZwDAazJj/FQKRnfugfHMeRiTz0K3Ixam1HQKcPC+Fisu9NK1P08Uj4DleHgMdXC+WQ7nu3UEOhFMfTQcVUvQ1H4IN2sj8H2k7K+2TqCc3GseyA8AmDOzMBq7D9bS8sAr6nEJdNt3UbHVF1XQGtmZew8bTPT6tWoD3KpsUvlR8NxUoEICMvl6KQo+xqCwcRs4T8Ax5c8bFExjbAgAjO7aS8VsLypgq3g5nWStjztAhWRVhqAqeB6IuKTClkw1eNYEbrCQQBwD8yGGOsAooogLYejQPKBi7UPF9DkH+ezd+o141ZkUPAOC+L9SAMivNc7q46YMNSLTe4n1kaQF4XD3ZIDTPgU3XEYciKcAHrsGJS1xKFBGgyVzouiT4VbdGhjt/cEA5isyKsaz7jl3we7bg7Rqf6j0LoSldON4wWcqJDgQNGTN++l13vELA+MK6kKd6iryFOvxtidt9i5gO7owdjKJQliflNAU1pas6xQgnAzg1ux+lJEdILixNr0Pq9JUUA8NwVG9DM73G0jlcnh+V4BpjIWzJmIGQIjnXw5TiDuKSEwxurm3ITc8DNO51BnrLbIcsrW0dNA6RxgUKU1UdGVqLy5X6qGzTLvlnewiBZyGs3Yz6X8UeaYI3olvZDhzwLumZ+eHvooCCC0Q5VUsb4unwycM4YIDqA01tPqmgbzQr2EIYPECiPm33LYoDiZSsY9moh9O/Znoa4d9HkXtPg2pX/cPKCoRQ+ocZa4AAAAASUVORK5CYII=</Image>
 <Url type="application/x-suggestions+json" method="GET" template="http://anywhere.ebay.com/services/suggest/">
   <Param name="s" value="0"/>
   <Param name="q" value="{searchTerms}"/>
 </Url>
-<Url type="text/html" method="GET" template="http://rover.ebay.com/rover/1/711-47294-18009-3/4">
+<Url type="text/html" method="GET" template="http://rover.ebay.com/rover/1/711-47294-18009-3/4" resultdomain="ebay.com">
   <Param name="mpre" value="http://shop.ebay.com/?_nkw={searchTerms}"/>
 </Url>
 <SearchForm>http://search.ebay.com/</SearchForm>
 </SearchPlugin>
 
--- a/browser/locales/en-US/searchplugins/wikipedia.xml
+++ b/browser/locales/en-US/searchplugins/wikipedia.xml
@@ -6,14 +6,14 @@
 <ShortName>Wikipedia (en)</ShortName>
 <Description>Wikipedia, the free encyclopedia</Description>
 <InputEncoding>UTF-8</InputEncoding>
 <Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAAAAAA4AQAAJgAAACAgAAAAAAAAJAMAAGQBAACJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAAEFSURBVDjLxZPRDYJAEESJoQjpgBoM/9IBtoAl4KcUQQlSAjYgJWAH0gPmyNtkzEEuxkQTPzawc3Ozc3MQTc/JfVPR/wW6a+eKQ+Hyfe54B2wvrfXVqXLDfTCMd3j0VHksrTcH9bl2aZq+BCgEwCCPj9E4TdPYGj0C9CYAKdkmBrIIxiIYbvpbb2sSl8AiA+ywAbJE5YLpCImLU/WRDyIAWRgu4k1s4v50ODru4haYSCk4ntkuM0wcMAINXiPKTJQ9CfgB40phBr8DyFjGKkKEhYhCY4iCDgpAYAM2EZBlhJnsZxQUYBNkSkfBvjDd0ttPeR0mxREQ+OhfYOJ6EmL+l/qzn2kGli9cAF3BOfkAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAAIKSURBVFjD7ZdBSgNRDIYLguAB7FLwAkXwBl0JgiDYjQcY8ARduBJKu3I5C0EoWDxAT9AL9AK9QBeCIHQlCM/3DZOSmeZNZ2r1bQyEGV7yXv7kJZlJq6XIOXfs+crzwPPTnvnR863n05ZFufDD/T595Q4eauM37u/pWYwfeX53cegcABcuHg0AkEQE8AKAu4gAXv8BrAEMh0PXbrddt9t1vV4v406nk62laeqm02n2LjKYIuK5WCyyfeiLDF32yLn6TJ5mBFarlev3+9nBMMqsabkYhmezWcEd2ctTE/tYBwhgt14BhtmAV2VaLpdrAHioCW+VdwWy9IMAUBQjJcQFTwGqvcTD+Xy+oc8askZJyAYrnKEokCeWLpQkSSZvBIANYgSDVVEQQJaeyHQu1QIgiQNb6AmrTtaQ9+RFSLa1D4iXgfsrVITloeSFFZlaAEjAUMaXo2DJWQtVRe1OKF5aJUkf0NdglXO5VzQGoI2USwwD3LEl590CtdO3QBoT5WSFV+Q63Oha17ITgMlkslGSGBWPdeNiDR2SL1B6zQFINmOAkFOW5eTSURCdvX6OdUlapaWjsKX0dgOg26/VWHSUKhrPz35ISKwq76R9Wx+kKgC1f0o5mISsypUG3kPj2L/lDzKYvEUwzoh2JtPRdQQAo1jD6afne88H1oTMeH6ZK+x7PB/lQ/CJtvkNEgDh1dr/bVYAAAAASUVORK5CYII=</Image>
 <Url type="application/x-suggestions+json" method="GET" template="http://en.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
-<Url type="text/html" method="GET" template="http://en.wikipedia.org/wiki/Special:Search">
+<Url type="text/html" method="GET" template="http://en.wikipedia.org/wiki/Special:Search" resultdomain="wikipedia.org">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
 <SearchForm>http://en.wikipedia.org/wiki/Special:Search</SearchForm>
 </SearchPlugin>
--- a/browser/locales/en-US/searchplugins/yahoo.xml
+++ b/browser/locales/en-US/searchplugins/yahoo.xml
@@ -4,15 +4,15 @@
 
 <SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
 <ShortName>Yahoo</ShortName>
 <Description>Yahoo Search</Description>
 <InputEncoding>UTF-8</InputEncoding>
 <Image width="16" height="16">data:image/x-icon;base64,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</Image>
 <Url type="application/x-suggestions+json" method="GET"
      template="http://ff.search.yahoo.com/gossip?output=fxjson&amp;command={searchTerms}" />
-<Url type="text/html" method="GET" template="http://search.yahoo.com/search">
+<Url type="text/html" method="GET" template="http://search.yahoo.com/search" resultdomain="yahoo.com">
   <Param name="p" value="{searchTerms}"/>
   <Param name="ei" value="UTF-8"/>
   <MozParam name="fr" condition="pref" pref="yahoo-fr" />
 </Url>
 <SearchForm>http://search.yahoo.com/</SearchForm>
 </SearchPlugin>
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -11,31 +11,35 @@ EXTRA_JS_MODULES += [
     'BrowserUITelemetry.jsm',
     'ContentClick.jsm',
     'ContentLinkHandler.jsm',
     'CustomizationTabPreloader.jsm',
     'Feeds.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'SharedFrame.jsm',
-    'SignInToWebsite.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TabCrashReporter.jsm',
     'WebappManager.jsm',
     'webrtcUI.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [
         'Windows8WindowFrameColor.jsm',
         'WindowsJumpLists.jsm',
         'WindowsPreviewPerTab.jsm',
     ]
 
+if CONFIG['NIGHTLY_BUILD']:
+    EXTRA_JS_MODULES += [
+        'SignInToWebsite.jsm',
+    ]
+
 EXTRA_PP_JS_MODULES += [
     'AboutHome.jsm',
     'RecentWindow.jsm',
     'UITour.jsm',
 ]
 
 if CONFIG['MOZILLA_OFFICIAL']:
     DEFINES['MOZILLA_OFFICIAL'] = 1
--- a/browser/modules/test/browser_SignInToWebsite.js
+++ b/browser/modules/test/browser_SignInToWebsite.js
@@ -262,22 +262,32 @@ function test_auth() {
 
   Services.obs.notifyObservers({ wrappedJSObject: notifyOptions },
                                "identity-auth", TEST_ORIGIN + "/auth");
 }
 
 function test() {
   waitForExplicitFinish();
 
+  let sitw = {};
+  try {
+    Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw);
+  } catch (ex) {
+    ok(true, "Skip the test since SignInToWebsite.jsm isn't packaged outside outside mozilla-central");
+    finish();
+    return;
+  }
+
   registerCleanupFunction(cleanUp);
 
-  let sitw = {};
-  Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw);
-
   ok(sitw.SignInToWebsiteUX, "SignInToWebsiteUX object exists");
+  if (!Services.prefs.getBoolPref("dom.identity.enabled")) {
+    // If the pref isn't enabled then init wasn't called so do that for the test.
+    sitw.SignInToWebsiteUX.init();
+  }
 
   // Replace implementation of ID Service functions for testing
   window.selectIdentity = sitw.SignInToWebsiteUX.selectIdentity;
   sitw.SignInToWebsiteUX.selectIdentity = function(aRpId, aIdentity) {
     info("Identity selected: " + aIdentity);
     window.gIdentitySelected = {rpId: aRpId, identity: aIdentity};
   };
 
@@ -312,16 +322,19 @@ function cleanUp() {
   // Put the JSM functions back to how they were
   IdentityService.IDP.setAuthenticationFlow = window.setAuthenticationFlow;
   delete window.setAuthenticationFlow;
 
   let sitw = {};
   Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw);
   sitw.SignInToWebsiteUX.selectIdentity = window.selectIdentity;
   delete window.selectIdentity;
+  if (!Services.prefs.getBoolPref("dom.identity.enabled")) {
+    sitw.SignInToWebsiteUX.uninit();
+  }
 
   Services.prefs.clearUserPref("toolkit.identity.debug");
 }
 
 let gActiveListeners = {};
 let gActiveObservers = {};
 let gShownState = {};
 
new file mode 100644
--- /dev/null
+++ b/content/base/src/FeedWriterEnabled.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "js/TypeDecls.h"
+#include "nsGlobalWindow.h"
+#include "nsIPrincipal.h"
+#include "nsIURI.h"
+#include "nsString.h"
+#include "xpcpublic.h"
+
+namespace mozilla {
+
+struct FeedWriterEnabled {
+  static bool IsEnabled(JSContext* cx, JSObject* aGlobal)
+  {
+    // Make sure the global is a window
+    nsGlobalWindow* win = xpc::WindowGlobalOrNull(aGlobal);
+    if (!win) {
+      return false;
+    }
+
+    // Make sure that the principal is about:feeds.
+    nsCOMPtr<nsIPrincipal> principal = win->GetPrincipal();
+    NS_ENSURE_TRUE(principal, false);
+    nsCOMPtr<nsIURI> uri;
+    principal->GetURI(getter_AddRefs(uri));
+    if (!uri) {
+      return false;
+    }
+
+    // First check the scheme to avoid getting long specs in the common case.
+    bool isAbout = false;
+    uri->SchemeIs("about", &isAbout);
+    if (!isAbout) {
+      return false;
+    }
+
+    // Now check the spec itself
+    nsAutoCString spec;
+    uri->GetSpec(spec);
+    return spec.Equals("about:feeds");
+  }
+};
+
+}
--- a/content/base/src/moz.build
+++ b/content/base/src/moz.build
@@ -48,16 +48,20 @@ if CONFIG['MOZ_WEBRTC']:
     ]
 
 # Are we targeting x86-32 or x86-64?  If so, we want to include SSE2 code for
 # nsTextFragment.cpp
 if CONFIG['INTEL_ARCHITECTURE']:
     SOURCES += ['nsTextFragmentSSE2.cpp']
     SOURCES['nsTextFragmentSSE2.cpp'].flags += CONFIG['SSE2_FLAGS']
 
+EXPORTS.mozilla += [
+    'FeedWriterEnabled.h',
+]
+
 EXPORTS.mozilla.dom += [
     'Attr.h',
     'Comment.h',
     'DocumentFragment.h',
     'DocumentType.h',
     'DOMImplementation.h',
     'DOMParser.h',
     'DOMPoint.h',
--- a/content/media/fmp4/demuxer/basictypes.h
+++ b/content/media/fmp4/demuxer/basictypes.h
@@ -20,17 +20,17 @@ namespace mp4_demuxer {
 //#define LOG_DEMUXER
 
 #define kint32max std::numeric_limits<int32_t>::max()
 #define kuint64max std::numeric_limits<uint64_t>::max()
 #define kint64max std::numeric_limits<int64_t>::max()
 
 
 
-#define OVERRIDE override
+#define OVERRIDE MOZ_OVERRIDE
 #define WARN_UNUSED_RESULT
 
 #define DCHECK(condition) \
 { \
   if (!(condition)) {\
     DMX_LOG("DCHECK Failed (%s) %s:%d\n", #condition, __FILE__, __LINE__); \
   } \
 }
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -612,12 +612,12 @@ if CONFIG['MOZ_GAMEPAD']:
     ]
 
 if CONFIG['MOZ_B2G_BT']:
     GENERATED_EVENTS_WEBIDL_FILES += [
         'BluetoothDeviceEvent.webidl',
         'BluetoothStatusChangedEvent.webidl',
     ]
 
-if CONFIG['MOZ_BUILD_APP'] == 'browser':
+if CONFIG['MOZ_BUILD_APP'] in ['browser', 'xulrunner']:
     WEBIDL_FILES += [
         'BrowserFeedWriter.webidl',
     ]
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -1569,16 +1569,26 @@ nsXPCComponents_utils_Sandbox::CallOrCon
 
     SandboxOptions options(cx, optionsObject);
     if (calledWithOptions && !options.Parse())
         return ThrowAndFail(NS_ERROR_INVALID_ARG, cx, _retval);
 
     if (NS_FAILED(AssembleSandboxMemoryReporterName(cx, options.sandboxName)))
         return ThrowAndFail(NS_ERROR_INVALID_ARG, cx, _retval);
 
+    if (options.metadata.isNullOrUndefined()) {
+        // If the caller is running in a sandbox, inherit.
+        RootedObject callerGlobal(cx, CurrentGlobalOrNull(cx));
+        if (IsSandbox(callerGlobal)) {
+            rv = GetSandboxMetadata(cx, callerGlobal, &options.metadata);
+            if (NS_WARN_IF(NS_FAILED(rv)))
+                return rv;
+        }
+    }
+
     rv = CreateSandboxObject(cx, args.rval(), prinOrSop, options);
 
     if (NS_FAILED(rv))
         return ThrowAndFail(rv, cx, _retval);
 
     *_retval = true;
     return NS_OK;
 }
--- a/js/xpconnect/tests/unit/test_sandbox_metadata.js
+++ b/js/xpconnect/tests/unit/test_sandbox_metadata.js
@@ -36,10 +36,21 @@ function run_test()
 
   try {
     Components.utils.setSandboxMetadata(sandbox, { foo: reflector });
   } catch(e) {
     thrown = true;
   }
 
   do_check_eq(thrown, true);
+
+  sandbox = Components.utils.Sandbox(this, {
+    metadata: { foopy: { bar: 2 }, baz: "hi" }
+  });
+
+  let inner = Components.utils.evalInSandbox("Components.utils.Sandbox('http://www.blah.com')", sandbox);
+
+  metadata = Components.utils.getSandboxMetadata(inner);
+  do_check_eq(metadata.baz, "hi");
+  do_check_eq(metadata.foopy.bar, 2);
+  metadata.baz = "foo";
 }
 
--- a/layout/reftests/svg/reftest.list
+++ b/layout/reftests/svg/reftest.list
@@ -220,17 +220,17 @@ random-if(gtk2Widget) == objectBoundingB
 == objectBoundingBox-and-pattern-01b.svg objectBoundingBox-and-pattern-01-ref.svg
 == objectBoundingBox-and-pattern-01c.svg objectBoundingBox-and-pattern-01-ref.svg
 == objectBoundingBox-and-pattern-02.svg pass.svg
 == objectBoundingBox-and-pattern-03.svg objectBoundingBox-and-pattern-03-ref.svg
 == opacity-and-gradient-01.svg pass.svg
 skip-if(d2d) fuzzy-if(azureQuartz,1,99974) == opacity-and-gradient-02.svg opacity-and-gradient-02-ref.svg
 == opacity-and-pattern-01.svg pass.svg
 == opacity-and-transform-01.svg opacity-and-transform-01-ref.svg
-fuzzy-if(Android&&AndroidVersion>=15,8,200) == outer-svg-border-and-padding-01.svg outer-svg-border-and-padding-01-ref.svg 
+fails-if(B2G) fuzzy-if(Android&&AndroidVersion>=15,8,200) == outer-svg-border-and-padding-01.svg outer-svg-border-and-padding-01-ref.svg # B2G scrollbar difference
 == overflow-on-outer-svg-01.svg overflow-on-outer-svg-01-ref.svg
 == overflow-on-outer-svg-02a.xhtml overflow-on-outer-svg-02-ref.xhtml
 == overflow-on-outer-svg-02b.xhtml overflow-on-outer-svg-02-ref.xhtml
 == overflow-on-outer-svg-02c.xhtml overflow-on-outer-svg-02-ref.xhtml
 == overflow-on-outer-svg-02d.xhtml overflow-on-outer-svg-02-ref.xhtml
 == overflow-on-outer-svg-03a.xhtml overflow-on-outer-svg-03-ref.xhtml
 == overflow-on-outer-svg-03b.xhtml overflow-on-outer-svg-03-ref.xhtml
 pref(svg.paint-order.enabled,true) == paint-order-01.svg paint-order-01-ref.svg
--- a/layout/reftests/z-index/reftest.list
+++ b/layout/reftests/z-index/reftest.list
@@ -1,11 +1,8 @@
-# Make overlay scrollbars never fade out
-default-preferences pref(layout.testing.overlay-scrollbars.always-visible,true)
-
 == 480053-1.html 480053-1-ref.html
 == z-index-1.html z-index-1-ref.html
 != stacking-context-yes.html stacking-context-no.html
 == stacking-context-perspective.html stacking-context-yes.html
 == stacking-context-backface-visibility.html stacking-context-no.html
 
 fails-if(Android) != overlayscrollbar-sorting-ref-visible.html overlayscrollbar-sorting-ref-hidden.html
 == overlayscrollbar-sorting-1.html overlayscrollbar-sorting-ref-visible.html
--- a/layout/tools/reftest/b2g_start_script.js
+++ b/layout/tools/reftest/b2g_start_script.js
@@ -33,16 +33,20 @@ function setDefaultPrefs() {
     branch.setIntPref("urlclassifier.updateinterval", 172800);
     // Disable high-quality downscaling, since it makes reftests more difficult.
     branch.setBoolPref("image.high_quality_downscaling.enabled", false);
     // Checking whether two files are the same is slow on Windows.
     // Setting this pref makes tests run much faster there.
     branch.setBoolPref("security.fileuri.strict_origin_policy", false);
     // Disable the thumbnailing service
     branch.setBoolPref("browser.pagethumbnails.capturing_disabled", true);
+    // Disable the fade out (over time) of overlay scrollbars, since we
+    // can't guarantee taking both reftest snapshots at the same point
+    // during the fade.
+    branch.setBoolPref("layout.testing.overlay-scrollbars.always-visible", true);
 }
 
 function setPermissions() {
   if (__marionetteParams.length < 2) {
     return;
   }
 
   let serverAddr = __marionetteParams[0];
--- a/layout/tools/reftest/bootstrap.js
+++ b/layout/tools/reftest/bootstrap.js
@@ -33,16 +33,20 @@ function setDefaultPrefs() {
     branch.setBoolPref("extensions.update.enabled", false);
     branch.setBoolPref("extensions.getAddons.cache.enabled", false);
     // Disable blocklist updates so we don't have them reported as leaks
     branch.setBoolPref("extensions.blocklist.enabled", false);
     // Make url-classifier updates so rare that they won't affect tests
     branch.setIntPref("urlclassifier.updateinterval", 172800);
     // Disable high-quality downscaling, since it makes reftests more difficult.
     branch.setBoolPref("image.high_quality_downscaling.enabled", false);
+    // Disable the fade out (over time) of overlay scrollbars, since we
+    // can't guarantee taking both reftest snapshots at the same point
+    // during the fade.
+    branch.setBoolPref("layout.testing.overlay-scrollbars.always-visible", true);
 }
 
 var windowListener = {
     onOpenWindow: function(aWindow) {
         let domWindow = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowInternal || Components.interfaces.nsIDOMWindow);
         domWindow.addEventListener("load", function() {
             domWindow.removeEventListener("load", arguments.callee, false);
 
--- a/layout/tools/reftest/reftest-cmdline.js
+++ b/layout/tools/reftest/reftest-cmdline.js
@@ -109,16 +109,20 @@ RefTestCmdLineHandler.prototype =
     branch.setBoolPref("browser.pagethumbnails.capturing_disabled", true);
     // Enable APZC so we can test it
     branch.setBoolPref("layers.async-pan-zoom.enabled", true);
     // Since our tests are 800px wide, set the assume-designed-for width of all
     // pages to be 800px (instead of the default of 980px). This ensures that
     // in our 800px window we don't zoom out by default to try to fit the
     // assumed 980px content.
     branch.setIntPref("browser.viewport.desktopWidth", 800);
+    // Disable the fade out (over time) of overlay scrollbars, since we
+    // can't guarantee taking both reftest snapshots at the same point
+    // during the fade.
+    branch.setBoolPref("layout.testing.overlay-scrollbars.always-visible", true);
 
     var wwatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                            .getService(nsIWindowWatcher);
 
     function loadReftests() {
       wwatch.openWindow(null, "chrome://reftest/content/reftest.xul", "_blank",
                         "chrome,dialog=no,all", args);
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/docs/index.rst
@@ -0,0 +1,80 @@
+===================
+Firefox for Android
+===================
+
+UI Telemetry
+============
+
+Fennec records UI events using a telemetry framework called UITelemetry.
+
+Some links:
+
+- `Project page <https://wiki.mozilla.org/Mobile/Projects/Telemetry_probes_for_Fennec_UI_elements>`_
+- `Wiki page <https://wiki.mozilla.org/Mobile/Fennec/Android/UITelemetry>`_
+- `User research notes <https://wiki.mozilla.org/Mobile/User_Experience/Research>`_
+
+Sessions
+--------
+
+**Sessions** are essentially scopes. They are meant to provide context to
+events; this allows events to be simpler and more reusable. Sessions are
+usually bound to some component of the UI, some user action with a duration, or
+some transient state.
+
+For example, a session might be begun when a user begins interacting with a
+menu, and stopped when the interaction ends. Or a session might encapsulate
+period of no network connectivity, the first five seconds after the browser
+launched, the time spent with an active download, or a guest mode session.
+
+Sessions implicitly record the duration of the interaction.
+
+A simple use-case for sessions is the bookmarks panel in about:home. We start a
+session when the user swipes into the panel, and stop it when they swipe away.
+This bookmarks session does two things: firstly, it gives scope to any generic
+event that may occur within the panel (*e.g.*, loading a URL). Secondly, it
+allows us to figure out how much time users are spending in the bookmarks
+panel.
+
+To start a session, call ``Telemetry.startUISession(String sessionName)``.
+Session names should be brief, lowercase, and should describe which UI
+component the user is interacting with. In certain cases where the UI component
+is dynamic, they could include an ID, essential to identifying that component.
+An example of this is dynamic home panels: we use session names of the format
+``homepanel:<panel_id>`` to identify home panel sessions.
+
+To stop a session call ``Telemetry.stopUISession(String sessionName, String
+reason)``. ``sessionName`` is the name of the open session and ``reason`` is a
+descriptive cause for the ending of the session. It should be brief, lowercase,
+and generic so it can be reused in different places. Examples reasons are:
+
+``switched``
+  The user transitioned to a UI element of equal level.
+
+``exit``
+  The user left for an entirely different element. 
+
+
+Events
+------
+
+Events capture key occurrences. They should be brief and simple, and should not contain sensitive or excess information. Context for events should come from the session (scope). An event can be created with four fields (via ``Telemetry.sendUIEvent``): ``action``, ``method``, ``extras``, and ``timestamp``.
+
+``action``
+  The name of the event. Should be brief and lowercase. If needed, you can make use of namespacing with a '``.``' separator. Example event names: ``panel.switch``, ``panel.enable``, ``panel.disable``, ``panel.install``. 
+
+``method`` (Optional)
+  Used for user actions that can be performed in many ways. This field specifies the method by which the action was performed. For example, users can add an item to their reading list either by long-tapping the reader icon in the address bar, or from within reader mode. We would use the same event name for both user actions but specify two methods: ``addressbar`` and ``readermode``. 
+
+``extras`` (Optional)
+  For extra information that may be useful in understanding the event. Make an effort to keep this brief. 
+
+``timestamp`` (Optional)
+  The time at which the event occurred. If not specified, this field defaults to the current value of the realtime clock. 
+  
+
+Clock
+-----
+
+Times are relative to either elapsed realtime (an arbitrary monotonically increasing clock that continues to tick when the device is asleep), or elapsed uptime (which doesn't tick when the device is in deep sleep). We default to elapsed realtime.
+
+See the documentation in `the source <http://mxr.mozilla.org/mozilla-central/source/mobile/android/base/Telemetry.java>`_ for more details. 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DIRS += ['locales']
+SPHINX_TREES['fennec'] = 'docs'
 
 include('android-services.mozbuild')
 
 thirdparty_source_dir = TOPSRCDIR + '/mobile/android/thirdparty/'
 
 resjar = add_java_jar('gecko-R')
 resjar.sources = []
 resjar.generated_sources += [
--- a/netwerk/base/public/nsIBrowserSearchService.idl
+++ b/netwerk/base/public/nsIBrowserSearchService.idl
@@ -17,17 +17,17 @@ interface nsISearchSubmission : nsISuppo
   readonly attribute nsIInputStream postData;
 
   /**
    * The URI to submit a search to.
    */
   readonly attribute nsIURI uri;
 };
 
-[scriptable, uuid(7914c4b8-f05b-40c9-a982-38a058cd1769)]
+[scriptable, uuid(77de6680-57ec-4105-a183-cc7cf7e84b09)]
 interface nsISearchEngine : nsISupports
 {
   /**
    * Gets a nsISearchSubmission object that contains information about what to
    * send to the search engine, including the URI and postData, if applicable.
    *
    * @param  data
    *         Data to add to the submission object.
@@ -152,16 +152,28 @@ interface nsISearchEngine : nsISupports
   readonly attribute long type;
 
   /**
    * An optional unique identifier for this search engine within the context of
    * the distribution, as provided by the distributing entity.
    */
   readonly attribute AString identifier;
 
+  /**
+   * Gets a string representing the hostname from which search results for a
+   * given responseType are returned, minus the leading "www." (if present).
+   * This can be specified as an url attribute in the engine description file,
+   * but will default to host from the <Url>'s template otherwise.
+   *
+   * @param  responseType [optional]
+   *         The MIME type to get resultDomain for.  Defaults to "text/html".
+   *
+   * @return the resultDomain for the given responseType.
+   */
+  AString getResultDomain([optional] in AString responseType);
 };
 
 [scriptable, uuid(9fc39136-f08b-46d3-b232-96f4b7b0e235)]
 interface nsISearchInstallCallback : nsISupports
 {
   const unsigned long ERROR_UNKNOWN_FAILURE = 0x1;
   const unsigned long ERROR_DUPLICATE_ENGINE = 0x2;
 
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -423,17 +423,17 @@ BookmarksEngine.prototype = {
   }
 };
 
 function BookmarksStore(name, engine) {
   Store.call(this, name, engine);
 
   // Explicitly nullify our references to our cached services so we don't leak
   Svc.Obs.add("places-shutdown", function() {
-    for each ([query, stmt] in Iterator(this._stmts)) {
+    for each (let [query, stmt] in Iterator(this._stmts)) {
       stmt.finalize();
     }
     this._stmts = {};
   }, this);
 }
 BookmarksStore.prototype = {
   __proto__: Store.prototype,
 
--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -20,16 +20,34 @@ Cu.import("resource://gre/modules/Task.j
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
   "resource://gre/modules/PlacesBackups.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
   "resource://gre/modules/Deprecated.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());
 XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder());
 
+/**
+ * Generates an hash for the given string.
+ *
+ * @note The generated hash is returned in base64 form.  Mind the fact base64
+ * is case-sensitive if you are going to reuse this code.
+ */
+function generateHash(aString) {
+  let cryptoHash = Cc["@mozilla.org/security/hash;1"]
+                     .createInstance(Ci.nsICryptoHash);
+  cryptoHash.init(Ci.nsICryptoHash.MD5);
+  let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+                       .createInstance(Ci.nsIStringInputStream);
+  stringStream.data = aString;
+  cryptoHash.updateFromStream(stringStream, -1);
+  // base64 allows the '/' char, but we can't use it for filenames.
+  return cryptoHash.finish(true).replace("/", "-", "g");
+}
+
 this.BookmarkJSONUtils = Object.freeze({
   /**
    * Import bookmarks from a url.
    *
    * @param aSpec
    *        url of the bookmark data.
    * @param aReplace
    *        Boolean if true, replace existing bookmarks, else merge.
@@ -94,23 +112,29 @@ this.BookmarkJSONUtils = Object.freeze({
     });
   },
 
   /**
    * Serializes bookmarks using JSON, and writes to the supplied file path.
    *
    * @param aFilePath
    *        OS.File path string for the "bookmarks.json" file to be created.
-   *
+   * @param [optional] aOptions
+   *        Object containing options for the export:
+   *         - failIfHashIs: if the generated file would have the same hash
+   *                         defined here, will reject with ex.becauseSameHash
    * @return {Promise}
-   * @resolves To the exported bookmarks count when the file has been created.
+   * @resolves once the file has been created, to an object with the
+   *           following properties:
+   *            - count: number of exported bookmarks
+   *            - hash: file hash for contents comparison
    * @rejects JavaScript exception.
    * @deprecated passing an nsIFile is deprecated
    */
-  exportToFile: function BJU_exportToFile(aFilePath) {
+  exportToFile: function BJU_exportToFile(aFilePath, aOptions={}) {
     if (aFilePath instanceof Ci.nsIFile) {
       Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.exportToFile " +
                          "is deprecated. Please use an OS.File path string instead.",
                          "https://developer.mozilla.org/docs/JavaScript_OS.File");
       aFilePath = aFilePath.path;
     }
     return Task.spawn(function* () {
       let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
@@ -120,22 +144,39 @@ this.BookmarkJSONUtils = Object.freeze({
       try {
         Services.telemetry
                 .getHistogramById("PLACES_BACKUPS_TOJSON_MS")
                 .add(Date.now() - startTime);
       } catch (ex) {
         Components.utils.reportError("Unable to report telemetry.");
       }
 
+      startTime = Date.now();
+      let hash = generateHash(jsonString);
+      // Report the time taken to generate the hash.
+      try {
+        Services.telemetry
+                .getHistogramById("PLACES_BACKUPS_HASHING_MS")
+                .add(Date.now() - startTime);
+      } catch (ex) {
+        Components.utils.reportError("Unable to report telemetry.");
+      }
+
+      if (hash === aOptions.failIfHashIs) {
+        let e = new Error("Hash conflict");
+        e.becauseSameHash = true;
+        throw e;
+      }
+
       // Do not write to the tmp folder, otherwise if it has a different
       // filesystem writeAtomic will fail.  Eventual dangling .tmp files should
       // be cleaned up by the caller.
       yield OS.File.writeAtomic(aFilePath, jsonString,
                                 { tmpPath: OS.Path.join(aFilePath + ".tmp") });
-      return count;
+      return { count: count, hash: hash };
     });
   },
 
   /**
    * Serializes the given node (and all its descendents) as JSON
    * and writes the serialization to the given output stream.
    *
    * @param   aNode
--- a/toolkit/components/places/PlacesBackups.jsm
+++ b/toolkit/components/places/PlacesBackups.jsm
@@ -23,22 +23,81 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
   "resource://gre/modules/Sqlite.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "localFileCtor",
   () => Components.Constructor("@mozilla.org/file/local;1",
                                "nsILocalFile", "initWithPath"));
 
+XPCOMUtils.defineLazyGetter(this, "filenamesRegex",
+  () => new RegExp("^bookmarks-([0-9\-]+)(?:_([0-9]+)){0,1}(?:_([a-z0-9=\+\-]{24})){0,1}\.(json|html)", "i")
+);
+
+/**
+ * Appends meta-data information to a given filename.
+ */
+function appendMetaDataToFilename(aFilename, aMetaData) {
+  let matches = aFilename.match(filenamesRegex);
+  return "bookmarks-" + matches[1] +
+                  "_" + aMetaData.count +
+                  "_" + aMetaData.hash +
+                  "." + matches[4];
+}
+
+/**
+ * Gets the hash from a backup filename.
+ *
+ * @return the extracted hash or null.
+ */
+function getHashFromFilename(aFilename) {
+  let matches = aFilename.match(filenamesRegex);
+  if (matches && matches[3])
+    return matches[3];
+  return null;
+}
+
+/**
+ * Given two filenames, checks if they contain the same date.
+ */
+function isFilenameWithSameDate(aSourceName, aTargetName) {
+  let sourceMatches = aSourceName.match(filenamesRegex);
+  let targetMatches = aTargetName.match(filenamesRegex);
+
+  return sourceMatches && targetMatches &&
+         sourceMatches[1] == targetMatches[1] &&
+         sourceMatches[4] == targetMatches[4];
+}
+
+/**
+ * Given a filename, searches for another backup with the same date.
+ *
+ * @return OS.File path string or null.
+ */
+function getBackupFileForSameDate(aFilename) {
+  return Task.spawn(function* () {
+    let backupFiles = yield PlacesBackups.getBackupFiles();
+    for (let backupFile of backupFiles) {
+      if (isFilenameWithSameDate(OS.Path.basename(backupFile), aFilename))
+        return backupFile;
+    }
+    return null;
+  });
+}
+
 this.PlacesBackups = {
-  get _filenamesRegex() {
-    delete this._filenamesRegex;
-    return this._filenamesRegex =
-      new RegExp("^(bookmarks)-([0-9-]+)(_[0-9]+)*\.(json|html)");
-  },
+  /**
+   * Matches the backup filename:
+   *  0: file name
+   *  1: date in form Y-m-d
+   *  2: bookmarks count
+   *  3: contents hash
+   *  4: file extension
+   */
+  get filenamesRegex() filenamesRegex,
 
   get folder() {
     Deprecated.warning(
       "PlacesBackups.folder is deprecated and will be removed in a future version",
       "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
     return this._folder;
   },
 
@@ -94,18 +153,17 @@ this.PlacesBackups = {
   get _entries() {
     delete this._entries;
     this._entries = [];
     let files = this._folder.directoryEntries;
     while (files.hasMoreElements()) {
       let entry = files.getNext().QueryInterface(Ci.nsIFile);
       // A valid backup is any file that matches either the localized or
       // not-localized filename (bug 445704).
-      let matches = entry.leafName.match(this._filenamesRegex);
-      if (!entry.isHidden() && matches) {
+      if (!entry.isHidden() && filenamesRegex.test(entry.leafName)) {
         // Remove bogus backups in future dates.
         if (this.getDateForFile(entry) > new Date()) {
           entry.remove(false);
           continue;
         }
         this._entries.push(entry);
       }
     }
@@ -134,18 +192,17 @@ this.PlacesBackups = {
       yield iterator.forEach(function(aEntry) {
         // Since this is a lazy getter and OS.File I/O is serialized, we can
         // safely remove .tmp files without risking to remove ongoing backups.
         if (aEntry.name.endsWith(".tmp")) {
           OS.File.remove(aEntry.path);
           return;
         }
 
-        let matches = aEntry.name.match(this._filenamesRegex);
-        if (matches) {
+        if (filenamesRegex.test(aEntry.name)) {
           // Remove bogus backups in future dates.
           let filePath = aEntry.path;
           if (this.getDateForFile(filePath) > new Date()) {
             return OS.File.remove(filePath);
           } else {
             this._backupFiles.push(filePath);
           }
         }
@@ -183,20 +240,20 @@ this.PlacesBackups = {
    *
    * @param aBackupFile
    *        nsIFile or string path of the backup.
    * @return A Date object for the backup's creation time.
    */
   getDateForFile: function PB_getDateForFile(aBackupFile) {
     let filename = (aBackupFile instanceof Ci.nsIFile) ? aBackupFile.leafName
                                                        : OS.Path.basename(aBackupFile);
-    let matches = filename.match(this._filenamesRegex);
+    let matches = filename.match(filenamesRegex);
     if (!matches)
       throw("Invalid backup file name: " + filename);
-    return new Date(matches[2].replace(/-/g, "/"));
+    return new Date(matches[1].replace(/-/g, "/"));
   },
 
   /**
    * Get the most recent backup file.
    *
    * @param [optional] aFileExt
    *                   Force file extension.  Either "html" or "json".
    *                   Will check for both if not defined.
@@ -253,188 +310,176 @@ this.PlacesBackups = {
   saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFilePath) {
     if (aFilePath instanceof Ci.nsIFile) {
       Deprecated.warning("Passing an nsIFile to PlacesBackups.saveBookmarksToJSONFile " +
                          "is deprecated. Please use an OS.File path instead.",
                          "https://developer.mozilla.org/docs/JavaScript_OS.File");
       aFilePath = aFilePath.path;
     }
     return Task.spawn(function* () {
-      let nodeCount = yield BookmarkJSONUtils.exportToFile(aFilePath);
+      let { count: nodeCount, hash: hash } =
+        yield BookmarkJSONUtils.exportToFile(aFilePath);
 
       let backupFolderPath = yield this.getBackupFolder();
       if (OS.Path.dirname(aFilePath) == backupFolderPath) {
         // We are creating a backup in the default backups folder,
         // so just update the internal cache.
         this._entries.unshift(new localFileCtor(aFilePath));
         if (!this._backupFiles) {
           yield this.getBackupFiles();
         }
         this._backupFiles.unshift(aFilePath);
       } else {
         // If we are saving to a folder different than our backups folder, then
         // we also want to copy this new backup to it.
         // This way we ensure the latest valid backup is the same saved by the
         // user.  See bug 424389.
-        let name = this.getFilenameForDate();
-        let newFilename = this._appendMetaDataToFilename(name,
-                                                         { nodeCount: nodeCount });
-        let newFilePath = OS.Path.join(backupFolderPath, newFilename);
-        let backupFile = yield this._getBackupFileForSameDate(name);
-        if (!backupFile) {
-          // Update internal cache if we are not replacing an existing
-          // backup file.
-          this._entries.unshift(new localFileCtor(newFilePath));
-          if (!this._backupFiles) {
-            yield this.getBackupFiles();
+        let mostRecentBackupFile = yield this.getMostRecentBackup("json");
+        if (!mostRecentBackupFile ||
+            hash != getHashFromFilename(OS.Path.basename(mostRecentBackupFile))) {
+          let name = this.getFilenameForDate();
+          let newFilename = appendMetaDataToFilename(name,
+                                                     { count: nodeCount,
+                                                       hash: hash });
+          let newFilePath = OS.Path.join(backupFolderPath, newFilename);
+          let backupFile = yield getBackupFileForSameDate(name);
+          if (backupFile) {
+            // There is already a backup for today, replace it.
+            yield OS.File.remove(backupFile, { ignoreAbsent: true });
+            if (!this._backupFiles)
+              yield this.getBackupFiles();
+            else
+              this._backupFiles.shift();
+            this._backupFiles.unshift(newFilePath);
+          } else {
+            // There is no backup for today, add the new one.
+            this._entries.unshift(new localFileCtor(newFilePath));
+            if (!this._backupFiles)
+              yield this.getBackupFiles();
+            this._backupFiles.unshift(newFilePath);
           }
-          this._backupFiles.unshift(newFilePath);
+
+          yield OS.File.copy(aFilePath, newFilePath);
         }
-
-        yield OS.File.copy(aFilePath, newFilePath);
       }
 
       return nodeCount;
     }.bind(this));
   },
 
   /**
    * Creates a dated backup in <profile>/bookmarkbackups.
    * Stores the bookmarks using JSON.
    * Note: any item that should not be backed up must be annotated with
    *       "places/excludeFromBackup".
    *
    * @param [optional] int aMaxBackups
-   *                       The maximum number of backups to keep.
+   *                       The maximum number of backups to keep.  If set to 0
+   *                       all existing backups are removed and aForceBackup is
+   *                       ignored, so a new one won't be created.
    * @param [optional] bool aForceBackup
    *                        Forces creating a backup even if one was already
    *                        created that day (overwrites).
    * @return {Promise}
    */
   create: function PB_create(aMaxBackups, aForceBackup) {
-    return Task.spawn(function* () {
-      // Construct the new leafname.
-      let newBackupFilename = this.getFilenameForDate();
-      let mostRecentBackupFile = yield this.getMostRecentBackup();
-
-      if (!aForceBackup) {
-        let backupFiles = yield this.getBackupFiles();
-        // If there are backups, limit them to aMaxBackups, if requested.
-        if (backupFiles.length > 0 && typeof aMaxBackups == "number" &&
-            aMaxBackups > -1 && backupFiles.length >= aMaxBackups) {
-          let numberOfBackupsToDelete = backupFiles.length - aMaxBackups;
+    let limitBackups = function* () {
+      let backupFiles = yield this.getBackupFiles();
+      if (typeof aMaxBackups == "number" && aMaxBackups > -1 &&
+          backupFiles.length >= aMaxBackups) {
+        let numberOfBackupsToDelete = backupFiles.length - aMaxBackups;
+        while (numberOfBackupsToDelete--) {
+          this._entries.pop();
+          let oldestBackup = this._backupFiles.pop();
+          yield OS.File.remove(oldestBackup);
+        }
+      }
+    }.bind(this);
 
-          // If we don't have today's backup, remove one more so that
-          // the total backups after this operation does not exceed the
-          // number specified in the pref.
-          if (!this._isFilenameWithSameDate(OS.Path.basename(mostRecentBackupFile),
-                                            newBackupFilename)) {
-            numberOfBackupsToDelete++;
-          }
-
-          while (numberOfBackupsToDelete--) {
-            this._entries.pop();
-            let oldestBackup = this._backupFiles.pop();
-            yield OS.File.remove(oldestBackup);
-          }
-        }
-
-        // Do nothing if we already have this backup or we don't want backups.
-        if (aMaxBackups === 0 ||
-            (mostRecentBackupFile &&
-             this._isFilenameWithSameDate(OS.Path.basename(mostRecentBackupFile),
-                                          newBackupFilename)))
-          return;
+    return Task.spawn(function* () {
+      if (aMaxBackups === 0) {
+        // Backups are disabled, delete any existing one and bail out.
+        yield limitBackups(0);
+        return;
       }
 
-      let backupFile = yield this._getBackupFileForSameDate(newBackupFilename);
+      // Ensure to initialize _backupFiles
+      if (!this._backupFiles)
+        yield this.getBackupFiles();
+      let newBackupFilename = this.getFilenameForDate();
+      // If we already have a backup for today we should do nothing, unless we
+      // were required to enforce a new backup.
+      let backupFile = yield getBackupFileForSameDate(newBackupFilename);
+      if (backupFile && !aForceBackup)
+        return;
+
       if (backupFile) {
-        if (aForceBackup) {
-          yield OS.File.remove(backupFile, { ignoreAbsent: true });
-        } else {
-          return;
-        }
+        // In case there is a backup for today we should recreate it.
+        this._backupFiles.shift();
+        this._entries.shift();
+        yield OS.File.remove(backupFile, { ignoreAbsent: true });
       }
 
+      // Now check the hash of the most recent backup, and try to create a new
+      // backup, if that fails due to hash conflict, just rename the old backup.
+      let mostRecentBackupFile = yield this.getMostRecentBackup();
+      let mostRecentHash = mostRecentBackupFile &&
+                           getHashFromFilename(OS.Path.basename(mostRecentBackupFile));
+
       // Save bookmarks to a backup file.
       let backupFolder = yield this.getBackupFolder();
       let newBackupFile = OS.Path.join(backupFolder, newBackupFilename);
-      let nodeCount = yield this.saveBookmarksToJSONFile(newBackupFile);
-      // Rename the filename with metadata.
-      let newFilenameWithMetaData = this._appendMetaDataToFilename(
-                                      newBackupFilename,
-                                      { nodeCount: nodeCount });
+      let newFilenameWithMetaData;
+      try {
+        let { count: nodeCount, hash: hash } =
+          yield BookmarkJSONUtils.exportToFile(newBackupFile,
+                                               { failIfHashIs: mostRecentHash });
+        newFilenameWithMetaData = appendMetaDataToFilename(newBackupFilename,
+                                                           { count: nodeCount,
+                                                             hash: hash });
+      } catch (ex if ex.becauseSameHash) {
+        // The last backup already contained up-to-date information, just
+        // rename it as if it was today's backup.
+        this._backupFiles.shift();
+        this._entries.shift();
+        newBackupFile = mostRecentBackupFile;
+        newFilenameWithMetaData = appendMetaDataToFilename(
+          newBackupFilename,
+          { count: this.getBookmarkCountForFile(mostRecentBackupFile),
+            hash: mostRecentHash });
+      }
+
+      // Append metadata to the backup filename.
       let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData);
       yield OS.File.move(newBackupFile, newBackupFileWithMetadata);
+      this._entries.unshift(new localFileCtor(newBackupFileWithMetadata));
+      this._backupFiles.unshift(newBackupFileWithMetadata);
 
-      // Update internal cache.
-      let newFileWithMetaData = new localFileCtor(newBackupFileWithMetadata);
-      this._entries.pop();
-      this._entries.unshift(newFileWithMetaData);
-      this._backupFiles.pop();
-      this._backupFiles.unshift(newBackupFileWithMetadata);
+      // Limit the number of backups.
+      yield limitBackups(aMaxBackups);
     }.bind(this));
   },
 
-  _appendMetaDataToFilename:
-  function PB__appendMetaDataToFilename(aFilename, aMetaData) {
-    let matches = aFilename.match(this._filenamesRegex);
-    let newFilename = matches[1] + "-" + matches[2] + "_" +
-                      aMetaData.nodeCount + "." + matches[4];
-    return newFilename;
-  },
-
   /**
    * Gets the bookmark count for backup file.
    *
    * @param aFilePath
    *        File path The backup file.
    *
    * @return the bookmark count or null.
    */
   getBookmarkCountForFile: function PB_getBookmarkCountForFile(aFilePath) {
     let count = null;
     let filename = OS.Path.basename(aFilePath);
-    let matches = filename.match(this._filenamesRegex);
-
-    if (matches && matches[3])
-      count = matches[3].replace(/_/g, "");
+    let matches = filename.match(filenamesRegex);
+    if (matches && matches[2])
+      count = matches[2];
     return count;
   },
 
-  _isFilenameWithSameDate:
-  function PB__isFilenameWithSameDate(aSourceName, aTargetName) {
-    let sourceMatches = aSourceName.match(this._filenamesRegex);
-    let targetMatches = aTargetName.match(this._filenamesRegex);
-
-    return (sourceMatches && targetMatches &&
-            sourceMatches[1] == targetMatches[1] &&
-            sourceMatches[2] == targetMatches[2] &&
-            sourceMatches[4] == targetMatches[4]);
-    },
-
-  _getBackupFileForSameDate:
-  function PB__getBackupFileForSameDate(aFilename) {
-    return Task.spawn(function* () {
-      let backupFolderPath = yield this.getBackupFolder();
-      let iterator = new OS.File.DirectoryIterator(backupFolderPath);
-      let backupFile;
-
-      yield iterator.forEach(function(aEntry) {
-        if (this._isFilenameWithSameDate(aEntry.name, aFilename)) {
-          backupFile = aEntry.path;
-          return iterator.close();
-        }
-      }.bind(this));
-      yield iterator.close();
-
-      return backupFile;
-    }.bind(this));
-  },
-
   /**
    * Gets a bookmarks tree representation usable to create backups in different
    * file formats.  The root or the tree is PlacesUtils.placesRootId.
    * Items annotated with PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO and all of their
    * descendants are excluded.
    *
    * @return an object representing a tree with the places root as its root.
    *         Each bookmark is represented by an object having these properties:
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/PriorityUrlProvider.jsm
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "PriorityUrlProvider" ];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+
+/**
+ * Provides search engines matches to the PriorityUrlProvider through the
+ * search engines definitions handled by the Search Service.
+ */
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+
+let SearchEnginesProvider = {
+  init: function () {
+    this._engines = new Map();
+    let deferred = Promise.defer();
+    Services.search.init(rv => {
+      if (Components.isSuccessCode(rv)) {
+        Services.search.getVisibleEngines().forEach(this._addEngine, this);
+        deferred.resolve();
+      } else {
+        deferred.reject(new Error("Unable to initialize search service."));
+      }
+    });
+    Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true);
+    return deferred.promise;
+  },
+
+  observe: function (engine, topic, verb) {
+    let engine = engine.QueryInterface(Ci.nsISearchEngine);
+    switch (verb) {
+      case "engine-added":
+        this._addEngine(engine);
+        break;
+      case "engine-changed":
+        if (engine.hidden) {
+          this._removeEngine(engine);
+        } else {
+          this._addEngine(engine);
+        }
+        break;
+      case "engine-removed":
+        this._removeEngine(engine);
+        break;
+    }
+  },
+
+  _addEngine: function (engine) {
+    if (this._engines.has(engine.name)) {
+      return;
+    }
+    let token = engine.getResultDomain();
+    if (!token) {
+      return;
+    }
+    let match = { token: token,
+                  // TODO (bug 557665): searchForm should provide an usable
+                  // url with affiliate code, if available.
+                  url: engine.searchForm,
+                  title: engine.name,
+                  iconUrl: engine.iconURI ? engine.iconURI.spec : null,
+                  reason: "search" }
+    this._engines.set(engine.name, match);
+    PriorityUrlProvider.addMatch(match);
+  },
+
+  _removeEngine: function (engine) {
+    if (!this._engines.has(engine.name)) {
+      return;
+    }
+    this._engines.delete(engine.name);
+    PriorityUrlProvider.removeMatchByToken(engine.getResultDomain());
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference])
+}
+
+/**
+ * The PriorityUrlProvider allows to match a given string to a list of
+ * urls that should have priority in url search components, like autocomplete.
+ * Each returned match is an object with the following properties:
+ *  - token: string used to match the search term to the url
+ *  - url: url string represented by the match
+ *  - title: title describing the match, or an empty string if not available
+ *  - iconUrl: url of the icon associated to the match, or null if not available
+ *  - reason: a string describing the origin of the match, for example if it
+ *            represents a search engine, it will be "search".
+ */
+let matches = new Map();
+
+let initialized = false;
+function promiseInitialized() {
+  if (initialized) {
+    return Promise.resolve();
+  }
+  return Task.spawn(function* () {
+    try {
+      yield SearchEnginesProvider.init();
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
+    initialized = true;
+  });
+}
+
+this.PriorityUrlProvider = Object.freeze({
+  addMatch: function (match) {
+    matches.set(match.token, match);
+  },
+
+  removeMatchByToken: function (token) {
+    matches.delete(token);
+  },
+
+  getMatchingSpec: function (searchToken) {
+    return Task.spawn(function* () {
+      yield promiseInitialized();
+      for (let [token, match] of matches.entries()) {
+        // Match at the beginning for now.  In future an aOptions argument may
+        // allow  to control the matching behavior.
+        if (token.startsWith(searchToken)) {
+          return match;
+        }
+      }
+      return null;
+    }.bind(this));
+  }
+});
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -63,16 +63,17 @@ if CONFIG['MOZ_PLACES']:
         'BookmarkHTMLUtils.jsm',
         'BookmarkJSONUtils.jsm',
         'ClusterLib.js',
         'ColorAnalyzer_worker.js',
         'ColorConversion.js',
         'PlacesBackups.jsm',
         'PlacesDBUtils.jsm',
         'PlacesTransactions.jsm',
+        'PriorityUrlProvider.jsm'
     ]
 
     EXTRA_PP_JS_MODULES += [
         'PlacesUtils.jsm',
     ]
 
     EXTRA_COMPONENTS += [
         'ColorAnalyzer.js',
--- a/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
+++ b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
@@ -22,25 +22,24 @@ add_task(function check_max_backups_is_r
   let jsonFile = yield OS.File.open(jsonPath, { truncate: true });
   jsonFile.close();
   do_check_true(yield OS.File.exists(jsonPath));
 
   // Export bookmarks to JSON.
   // Allow 2 backups, the older one should be removed.
   yield PlacesBackups.create(2);
   let backupFilename = PlacesBackups.getFilenameForDate();
-  let re = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
 
   let count = 0;
   let lastBackupPath = null;
   let iterator = new OS.File.DirectoryIterator(backupFolder);
   try {
     yield iterator.forEach(aEntry => {
       count++;
-      if (aEntry.name.match(re))
+      if (PlacesBackups.filenamesRegex.test(aEntry.name))
         lastBackupPath = aEntry.path;
     });
   } finally {
     iterator.close();
   }
 
   do_check_eq(count, 2);
   do_check_neq(lastBackupPath, null);
@@ -51,25 +50,24 @@ add_task(function check_max_backups_is_r
 add_task(function check_max_backups_greater_than_backups() {
   // Get bookmarkBackups directory
   let backupFolder = yield PlacesBackups.getBackupFolder();
 
   // Export bookmarks to JSON.
   // Allow 3 backups, none should be removed.
   yield PlacesBackups.create(3);
   let backupFilename = PlacesBackups.getFilenameForDate();
-  let re = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
 
   let count = 0;
   let lastBackupPath = null;
   let iterator = new OS.File.DirectoryIterator(backupFolder);
   try {
     yield iterator.forEach(aEntry => {
       count++;
-      if (aEntry.name.match(re))
+      if (PlacesBackups.filenamesRegex.test(aEntry.name))
         lastBackupPath = aEntry.path;
     });
   } finally {
     iterator.close();
   }
   do_check_eq(count, 2);
   do_check_neq(lastBackupPath, null);
 });
@@ -78,25 +76,24 @@ add_task(function check_max_backups_null
   // Get bookmarkBackups directory
   let backupFolder = yield PlacesBackups.getBackupFolder();
 
   // Export bookmarks to JSON.
   // Allow infinite backups, none should be removed, a new one is not created
   // since one for today already exists.
   yield PlacesBackups.create(null);
   let backupFilename = PlacesBackups.getFilenameForDate();
-  let re = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
 
   let count = 0;
   let lastBackupPath = null;
   let iterator = new OS.File.DirectoryIterator(backupFolder);
   try {
     yield iterator.forEach(aEntry => {
       count++;
-      if (aEntry.name.match(re))
+      if (PlacesBackups.filenamesRegex.test(aEntry.name))
         lastBackupPath = aEntry.path;
     });
   } finally {
     iterator.close();
   }
   do_check_eq(count, 2);
   do_check_neq(lastBackupPath, null);
 });
@@ -105,25 +102,24 @@ add_task(function check_max_backups_unde
   // Get bookmarkBackups directory
   let backupFolder = yield PlacesBackups.getBackupFolder();
 
   // Export bookmarks to JSON.
   // Allow infinite backups, none should be removed, a new one is not created
   // since one for today already exists.
   yield PlacesBackups.create();
   let backupFilename = PlacesBackups.getFilenameForDate();
-  let re = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
 
   let count = 0;
   let lastBackupPath = null;
   let iterator = new OS.File.DirectoryIterator(backupFolder);
   try {
     yield iterator.forEach(aEntry => {
       count++;
-      if (aEntry.name.match(re))
+      if (PlacesBackups.filenamesRegex.test(aEntry.name))
         lastBackupPath = aEntry.path;
     });
   } finally {
     iterator.close();
   }
   do_check_eq(count, 2);
   do_check_neq(lastBackupPath, null);
 });
--- a/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
+++ b/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
@@ -17,39 +17,37 @@ function run_test() {
       entry.remove(false);
     }
 
     // Create a json dummy backup in the future.
     let dateObj = new Date();
     dateObj.setYear(dateObj.getFullYear() + 1);
     let name = PlacesBackups.getFilenameForDate(dateObj);
     do_check_eq(name, "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + ".json");
-    let rx = new RegExp("^" + name.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
     let files = bookmarksBackupDir.directoryEntries;
     while (files.hasMoreElements()) {
       let entry = files.getNext().QueryInterface(Ci.nsIFile);
-      if (entry.leafName.match(rx))
+      if (PlacesBackups.filenamesRegex.test(entry.leafName))
         entry.remove(false);
     }
 
     let futureBackupFile = bookmarksBackupDir.clone();
     futureBackupFile.append(name);
     futureBackupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
     do_check_true(futureBackupFile.exists());
 
     do_check_eq((yield PlacesBackups.getBackupFiles()).length, 0);
 
     yield PlacesBackups.create();
     // Check that a backup for today has been created.
     do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1);
     let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
     do_check_neq(mostRecentBackupFile, null);
     let todayFilename = PlacesBackups.getFilenameForDate();
-    rx = new RegExp("^" + todayFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
-    do_check_true(OS.Path.basename(mostRecentBackupFile).match(rx).length > 0);
+    do_check_true(PlacesBackups.filenamesRegex.test(OS.Path.basename(mostRecentBackupFile)));
 
     // Check that future backup has been removed.
     do_check_false(futureBackupFile.exists());
 
     // Cleanup.
     mostRecentBackupFile = new FileUtils.File(mostRecentBackupFile);
     mostRecentBackupFile.remove(false);
     do_check_false(mostRecentBackupFile.exists());
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js
@@ -0,0 +1,59 @@
+/* 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/. */
+
+/**
+ * Checks that automatically created bookmark backups are discarded if they are
+ * duplicate of an existing ones.
+ */
+function run_test() {
+  run_next_test();
+}
+
+add_task(function() {
+  // Create a backup for yesterday in the backups folder.
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+  let dateObj = new Date();
+  dateObj.setDate(dateObj.getDate() - 1);
+  let oldBackupName = PlacesBackups.getFilenameForDate(dateObj);
+  let oldBackup = OS.Path.join(backupFolder, oldBackupName);
+  let {count: count, hash: hash} = yield BookmarkJSONUtils.exportToFile(oldBackup);
+  do_check_true(count > 0);
+  do_check_eq(hash.length, 24);
+  oldBackupName = oldBackupName.replace(/\.json/, "_" + count + "_" + hash + ".json");
+  yield OS.File.move(oldBackup, OS.Path.join(backupFolder, oldBackupName));
+
+  // Create a backup.
+  // This should just rename the existing backup, so in the end there should be
+  // only one backup with today's date.
+  yield PlacesBackups.create();
+
+  // Get the hash of the generated backup
+  let backupFiles = yield PlacesBackups.getBackupFiles();
+  do_check_eq(backupFiles.length, 1);
+  
+  let matches = OS.Path.basename(backupFiles[0]).match(PlacesBackups.filenamesRegex);
+  do_check_eq(matches[1], new Date().toLocaleFormat("%Y-%m-%d"));
+  do_check_eq(matches[2], count);
+  do_check_eq(matches[3], hash);
+
+  // Add a bookmark and create another backup.
+  let bookmarkId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder,
+                                                        uri("http://foo.com"),
+                                                        PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                                        "foo");
+  // We must enforce a backup since one for today already exists.  The forced
+  // backup will replace the existing one.
+  yield PlacesBackups.create(undefined, true);
+  do_check_eq(backupFiles.length, 1);
+  recentBackup = yield PlacesBackups.getMostRecentBackup();
+  do_check_neq(recentBackup, OS.Path.join(backupFolder, oldBackupName));
+  matches = OS.Path.basename(recentBackup).match(PlacesBackups.filenamesRegex);
+  do_check_eq(matches[1], new Date().toLocaleFormat("%Y-%m-%d"));
+  do_check_eq(matches[2], count + 1);
+  do_check_neq(matches[3], hash);
+
+  // Clean up
+  PlacesUtils.bookmarks.removeItem(bookmarkId);
+  yield PlacesBackups.create(0);
+});
--- a/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js
+++ b/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js
@@ -26,32 +26,32 @@ add_task(function test_saveBookmarksToJS
   let nodeCount = yield PlacesBackups.saveBookmarksToJSONFile(backupFile, true);
   do_check_true(nodeCount > 0);
   do_check_true(backupFile.exists());
   do_check_eq(backupFile.leafName, "bookmarks.json");
 
   // Ensure the backup would be copied to our backups folder when the original
   // backup is saved somewhere else.
   let recentBackup = yield PlacesBackups.getMostRecentBackup();
-  let todayFilename = PlacesBackups.getFilenameForDate();
-  do_check_eq(OS.Path.basename(recentBackup),
-              todayFilename.replace(/\.json/, "_" + nodeCount + ".json"));
+  let matches = OS.Path.basename(recentBackup).match(PlacesBackups.filenamesRegex);
+  do_check_eq(matches[2], nodeCount);
+  do_check_eq(matches[3].length, 24);
 
   // Clear all backups in our backups folder.
   yield PlacesBackups.create(0);
   do_check_eq((yield PlacesBackups.getBackupFiles()).length, 0);
 
   // Test create() which saves bookmarks with metadata on the filename.
   yield PlacesBackups.create();
   do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1);
 
   mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
   do_check_neq(mostRecentBackupFile, null);
-  let rx = new RegExp("^" + todayFilename.replace(/\.json/, "") + "_([0-9]+)\.json$");
-  let matches = OS.Path.basename(recentBackup).match(rx);
-  do_check_true(matches.length > 0 && parseInt(matches[1]) == nodeCount);
+  matches = OS.Path.basename(recentBackup).match(PlacesBackups.filenamesRegex);
+  do_check_eq(matches[2], nodeCount);
+  do_check_eq(matches[3].length, 24);
 
   // Cleanup
   backupFile.remove(false);
   yield PlacesBackups.create(0);
   PlacesUtils.bookmarks.removeItem(bookmarkId);
 });
 
--- a/toolkit/components/places/tests/bookmarks/xpcshell.ini
+++ b/toolkit/components/places/tests/bookmarks/xpcshell.ini
@@ -24,8 +24,9 @@ tail =
 [test_keywords.js]
 [test_nsINavBookmarkObserver.js]
 [test_removeItem.js]
 [test_savedsearches.js]
 [test_675416.js]
 [test_711914.js]
 [test_protectRoots.js]
 [test_818593-store-backup-metadata.js]
+[test_818584-discard-duplicate-backups.js]
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -517,20 +517,19 @@ function remove_all_JSON_backups() {
  */
 function check_JSON_backup(aIsAutomaticBackup) {
   let profileBookmarksJSONFile;
   if (aIsAutomaticBackup) {
     let bookmarksBackupDir = gProfD.clone();
     bookmarksBackupDir.append("bookmarkbackups");
     let files = bookmarksBackupDir.directoryEntries;
     let backup_date = new Date().toLocaleFormat("%Y-%m-%d");
-    let rx = new RegExp("^bookmarks-" + backup_date + "_[0-9]+\.json$");
     while (files.hasMoreElements()) {
       let entry = files.getNext().QueryInterface(Ci.nsIFile);
-      if (entry.leafName.match(rx)) {
+      if (PlacesBackups.filenamesRegex.test(entry.leafName)) {
         profileBookmarksJSONFile = entry;
         break;
       }
     }
   } else {
     profileBookmarksJSONFile = gProfD.clone();
     profileBookmarksJSONFile.append("bookmarkbackups");
     profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_priorityUrlProvider.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+Cu.import("resource://gre/modules/PriorityUrlProvider.jsm");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function* search_engine_match() {
+  let engine = yield promiseDefaultSearchEngine();
+  let token = engine.getResultDomain();
+  let match = yield PriorityUrlProvider.getMatchingSpec(token.substr(0, 1));
+  do_check_eq(match.url, engine.searchForm);
+  do_check_eq(match.title, engine.name);
+  do_check_eq(match.iconUrl, engine.iconURI ? engine.iconURI.spec : null);
+  do_check_eq(match.reason, "search");
+});
+
+add_task(function* no_match() {
+  do_check_eq(null, yield PriorityUrlProvider.getMatchingSpec("test"));
+});
+
+add_task(function* hide_search_engine_nomatch() {
+  let engine = yield promiseDefaultSearchEngine();
+  let token = engine.getResultDomain();
+  let promiseTopic = promiseSearchTopic("engine-changed");
+  Services.search.removeEngine(engine);
+  yield promiseTopic;
+  do_check_true(engine.hidden);
+  do_check_eq(null, yield PriorityUrlProvider.getMatchingSpec(token.substr(0, 1)));
+});
+
+add_task(function* add_search_engine_match() {
+  let promiseTopic = promiseSearchTopic("engine-added");
+  do_check_eq(null, yield PriorityUrlProvider.getMatchingSpec("bacon"));
+  Services.search.addEngineWithDetails("bacon", "", "bacon", "Search Bacon",
+                                       "GET", "http://www.bacon.moz/?search={searchTerms}");
+  yield promiseSearchTopic;
+  let match = yield PriorityUrlProvider.getMatchingSpec("bacon");
+  do_check_eq(match.url, "http://www.bacon.moz");
+  do_check_eq(match.title, "bacon");
+  do_check_eq(match.iconUrl, null);
+  do_check_eq(match.reason, "search");
+});
+
+add_task(function* remove_search_engine_nomatch() {
+  let engine = Services.search.getEngineByName("bacon");
+  let promiseTopic = promiseSearchTopic("engine-removed");
+  Services.search.removeEngine(engine);
+  yield promiseTopic;
+  do_check_eq(null, yield PriorityUrlProvider.getMatchingSpec("bacon"));
+});
+
+function promiseDefaultSearchEngine() {
+  let deferred = Promise.defer();
+  Services.search.init( () => {
+    deferred.resolve(Services.search.defaultEngine);
+  });
+  return deferred.promise;
+}
+
+function promiseSearchTopic(expectedVerb) {
+  let deferred = Promise.defer();
+  Services.obs.addObserver( function observe(subject, topic, verb) {
+    do_log_info("browser-search-engine-modified: " + verb);
+    if (verb == expectedVerb) {
+      Services.obs.removeObserver(observe, "browser-search-engine-modified");
+      deferred.resolve();
+    }
+  }, "browser-search-engine-modified", false);
+  return deferred.promise;
+}
--- a/toolkit/components/places/tests/unit/test_utils_backups_create.js
+++ b/toolkit/components/places/tests/unit/test_utils_backups_create.js
@@ -3,18 +3,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
  /**
   * Check for correct functionality of bookmarks backups
   */
 
-const PREFIX = "bookmarks-";
-const SUFFIX = ".json";
 const NUMBER_OF_BACKUPS = 10;
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function () {
   // Generate random dates.
@@ -35,17 +33,17 @@ add_task(function () {
   // Get and cleanup the backups folder.
   let backupFolderPath = yield PlacesBackups.getBackupFolder();
   let bookmarksBackupDir = new FileUtils.File(backupFolderPath);
 
   // Fake backups are created backwards to ensure we won't consider file
   // creation time.
   // Create fake backups for the newest dates.
   for (let i = dates.length - 1; i >= 0; i--) {
-    let backupFilename = PREFIX + dates[i] + SUFFIX;
+    let backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i]));
     let backupFile = bookmarksBackupDir.clone();
     backupFile.append(backupFilename);
     backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
     do_log_info("Creating fake backup " + backupFile.leafName);
     if (!backupFile.exists())
       do_throw("Unable to create fake backup " + backupFile.leafName);
   }
 
@@ -56,29 +54,28 @@ add_task(function () {
   // Check backups.  We have 11 dates but we the max number is 10 so the
   // oldest backup should have been removed.
   for (let i = 0; i < dates.length; i++) {
     let backupFilename;
     let shouldExist;
     let backupFile;
     if (i > 0) {
       let files = bookmarksBackupDir.directoryEntries;
-      let rx = new RegExp("^" + PREFIX + dates[i] + "(_[0-9]+){0,1}" + SUFFIX + "$");
       while (files.hasMoreElements()) {
         let entry = files.getNext().QueryInterface(Ci.nsIFile);
-        if (entry.leafName.match(rx)) {
+        if (PlacesBackups.filenamesRegex.test(entry.leafName)) {
           backupFilename = entry.leafName;
           backupFile = entry;
           break;
         }
       }
       shouldExist = true;
     }
     else {
-      backupFilename = PREFIX + dates[i] + SUFFIX;
+      backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i]));
       backupFile = bookmarksBackupDir.clone();
       backupFile.append(backupFilename);
       shouldExist = false;
     }
     if (backupFile.exists() != shouldExist)
       do_throw("Backup should " + (shouldExist ? "" : "not") + " exist: " + backupFilename);
   }
 
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -59,16 +59,17 @@ skip-if = os == "android"
 skip-if = os == "android"
 [test_adaptive_bug527311.js]
 [test_analyze.js]
 [test_annotations.js]
 [test_asyncExecuteLegacyQueries.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_async_history_api.js]
+[test_async_transactions.js]
 [test_autocomplete_stopSearch_no_throw.js]
 [test_bookmark_catobs.js]
 [test_bookmarks_json.js]
 [test_bookmarks_html.js]
 [test_bookmarks_html_corrupt.js]
 [test_bookmarks_html_singleframe.js]
 [test_bookmarks_restore_notification.js]
 [test_bookmarks_setNullTitle.js]
@@ -81,16 +82,17 @@ skip-if = os == "android"
 [test_download_history.js]
 # Bug 676989: test fails consistently on Android
 fail-if = os == "android"
 [test_frecency.js]
 [test_frecency_zero_updated.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_getChildIndex.js]
+[test_getPlacesInfo.js]
 [test_history.js]
 [test_history_autocomplete_tags.js]
 [test_history_catobs.js]
 [test_history_notifications.js]
 [test_history_observer.js]
 [test_history_removeAllPages.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
@@ -105,37 +107,36 @@ skip-if = os == "android"
 # Bug 676989: test fails consistently on Android
 fail-if = os == "android"
 [test_multi_word_tags.js]
 [test_nsINavHistoryViewer.js]
 # Bug 902248: intermittent timeouts on all platforms
 skip-if = true
 [test_null_interfaces.js]
 [test_onItemChanged_tags.js]
+[test_pageGuid_bookmarkGuid.js]
 [test_placeURIs.js]
+[test_PlacesUtils_asyncGetBookmarkIds.js]
+[test_PlacesUtils_lazyobservers.js]
+[test_placesTxn.js]
 [test_preventive_maintenance.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_preventive_maintenance_checkAndFixDatabase.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_preventive_maintenance_runTasks.js]
+[test_priorityUrlProvider.js]
 [test_removeVisitsByTimeframe.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_resolveNullBookmarkTitles.js]
 [test_result_sort.js]
 [test_sql_guid_functions.js]
 [test_tag_autocomplete_search.js]
 [test_tagging.js]
+[test_telemetry.js]
 [test_update_frecency_after_delete.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_utils_backups_create.js]
 [test_utils_getURLsForContainerNode.js]
 [test_utils_setAnnotationsFor.js]
-[test_PlacesUtils_asyncGetBookmarkIds.js]
-[test_PlacesUtils_lazyobservers.js]
-[test_placesTxn.js]
-[test_telemetry.js]
-[test_getPlacesInfo.js]
-[test_pageGuid_bookmarkGuid.js]
-[test_async_transactions.js]
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -856,22 +856,24 @@ function ParamSubstitution(aParamValue, 
  *        returned by this URL.
  * @param aMethod
  *        The HTTP request method. Must be a case insensitive value of either
  *        "GET" or "POST".
  * @param aTemplate
  *        The URL to which search queries should be sent. For GET requests,
  *        must contain the string "{searchTerms}", to indicate where the user
  *        entered search terms should be inserted.
+ * @param aResultDomain
+ *        The root domain for this URL.  Defaults to the template's host.
  *
  * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag
  *
  * @throws NS_ERROR_NOT_IMPLEMENTED if aType is unsupported.
  */
-function EngineURL(aType, aMethod, aTemplate) {
+function EngineURL(aType, aMethod, aTemplate, aResultDomain) {
   if (!aType || !aMethod || !aTemplate)
     FAIL("missing type, method or template for EngineURL!");
 
   var method = aMethod.toUpperCase();
   var type   = aType.toLowerCase();
 
   if (method != "GET" && method != "POST")
     FAIL("method passed to EngineURL must be \"GET\" or \"POST\"");
@@ -893,16 +895,24 @@ function EngineURL(aType, aMethod, aTemp
     // Disable these for now, see bug 295018
     // case "file":
     // case "resource":
       this.template = aTemplate;
       break;
     default:
       FAIL("new EngineURL: template uses invalid scheme!", Cr.NS_ERROR_FAILURE);
   }
+
+  // If no resultDomain was specified in the engine definition file, use the
+  // host from the template.
+  this.resultDomain = aResultDomain || templateURI.host;
+  // We never want to return a "www." prefix, so eventually strip it.
+  if (this.resultDomain.startsWith("www.")) {
+    this.resultDomain = this.resultDomain.substr(4);
+  }
 }
 EngineURL.prototype = {
 
   addParam: function SRCH_EURL_addParam(aName, aValue, aPurpose) {
     this.params.push(new QueryParameter(aName, aValue, aPurpose));
   },
 
   // Note: This method requires that aObj has a unique name or the previous MozParams entry with
@@ -1013,17 +1023,18 @@ EngineURL.prototype = {
 
   /**
    * Creates a JavaScript object that represents this URL.
    * @returns An object suitable for serialization as JSON.
    **/
   _serializeToJSON: function SRCH_EURL__serializeToJSON() {
     var json = {
       template: this.template,
-      rels: this.rels
+      rels: this.rels,
+      resultDomain: this.resultDomain
     };
 
     if (this.type != URLTYPE_SEARCH_HTML)
       json.type = this.type;
     if (this.method != "GET")
       json.method = this.method;
 
     function collapseMozParams(aParam)
@@ -1044,16 +1055,18 @@ EngineURL.prototype = {
    */
   _serializeToElement: function SRCH_EURL_serializeToEl(aDoc, aElement) {
     var url = aDoc.createElementNS(OPENSEARCH_NS_11, "Url");
     url.setAttribute("type", this.type);
     url.setAttribute("method", this.method);
     url.setAttribute("template", this.template);
     if (this.rels.length)
       url.setAttribute("rel", this.rels.join(" "));
+    if (this.resultDomain)
+      url.setAttribute("resultDomain", this.resultDomain);
 
     for (var i = 0; i < this.params.length; ++i) {
       var param = aDoc.createElementNS(OPENSEARCH_NS_11, "Param");
       param.setAttribute("name", this.params[i].name);
       param.setAttribute("value", this.params[i].value);
       url.appendChild(aDoc.createTextNode("\n  "));
       url.appendChild(param);
     }
@@ -1765,19 +1778,20 @@ Engine.prototype = {
    * @see EngineURL()
    */
   _parseURL: function SRCH_ENG_parseURL(aElement) {
     var type     = aElement.getAttribute("type");
     // According to the spec, method is optional, defaulting to "GET" if not
     // specified
     var method   = aElement.getAttribute("method") || "GET";
     var template = aElement.getAttribute("template");
+    var resultDomain = aElement.getAttribute("resultdomain");
 
     try {
-      var url = new EngineURL(type, method, template);
+      var url = new EngineURL(type, method, template, resultDomain);
     } catch (ex) {
       FAIL("_parseURL: failed to add " + template + " as a URL",
            Cr.NS_ERROR_FAILURE);
     }
 
     if (aElement.hasAttribute("rel"))
       url.rels = aElement.getAttribute("rel").toLowerCase().split(/\s+/);
 
@@ -2266,17 +2280,18 @@ Engine.prototype = {
       this._readOnly = true;
     else
       this._readOnly = false;
     this._iconURI = makeURI(aJson._iconURL);
     this._iconMapObj = aJson._iconMapObj;
     for (let i = 0; i < aJson._urls.length; ++i) {
       let url = aJson._urls[i];
       let engineURL = new EngineURL(url.type || URLTYPE_SEARCH_HTML,
-                                    url.method || "GET", url.template);
+                                    url.method || "GET", url.template,
+                                    url.resultDomain);
       engineURL._initWithJSON(url, this);
       this._urls.push(engineURL);
     }
   },
 
   /**
    * Creates a JavaScript object that represents this engine.
    * @param aFilter
@@ -2714,16 +2729,35 @@ Engine.prototype = {
     return url.getSubmission(data, this, aPurpose);
   },
 
   // from nsISearchEngine
   supportsResponseType: function SRCH_ENG_supportsResponseType(type) {
     return (this._getURLOfType(type) != null);
   },
 
+  // from nsISearchEngine
+  getResultDomain: function SRCH_ENG_getResultDomain(aResponseType) {
+#ifdef ANDROID
+    if (!aResponseType) {
+      aResponseType = this._defaultMobileResponseType;
+    }
+#endif
+    if (!aResponseType) {
+      aResponseType = URLTYPE_SEARCH_HTML;
+    }
+
+    LOG("getResultDomain: responseType: \"" + aResponseType + "\"");
+
+    let url = this._getURLOfType(aResponseType);
+    if (url)
+      return url.resultDomain;
+    return "";
+  },
+
   // nsISupports
   QueryInterface: function SRCH_ENG_QI(aIID) {
     if (aIID.equals(Ci.nsISearchEngine) ||
         aIID.equals(Ci.nsISupports))
       return this;
     throw Cr.NS_ERROR_NO_INTERFACE;
   },
 
--- a/toolkit/components/search/tests/xpcshell/data/engine.xml
+++ b/toolkit/components/search/tests/xpcshell/data/engine.xml
@@ -1,26 +1,26 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
 <ShortName>Test search engine</ShortName>
 <Description>A test search engine (based on Google search)</Description>
 <InputEncoding>UTF-8</InputEncoding>
 <Image width="16" height="16">data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA</Image>
 <Url type="application/x-suggestions+json" method="GET" template="http://suggestqueries.google.com/complete/search?output=firefox&amp;client=firefox&amp;hl={moz:locale}&amp;q={searchTerms}"/>
-<Url type="text/html" method="GET" template="http://www.google.com/search">
+<Url type="text/html" method="GET" template="http://www.google.com/search" resultdomain="google.com">
   <Param name="q" value="{searchTerms}"/>
   <Param name="ie" value="utf-8"/>
   <Param name="oe" value="utf-8"/>
   <Param name="aq" value="t"/>
   <!-- Dynamic parameters -->
   <MozParam name="client" condition="defaultEngine" trueValue="firefox-a" falseValue="firefox"/>
   <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
   <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
 </Url>
-<Url type="application/x-moz-default-purpose" method="GET" template="http://www.google.com/search">
+<Url type="application/x-moz-default-purpose" method="GET" template="http://www.google.com/search" resultdomain="purpose.google.com">
   <Param name="q" value="{searchTerms}"/>
   <MozParam name="client" condition="defaultEngine" trueValue="firefox-a" falseValue="firefox"/>
   <!-- MozParam with a default value if purpose is not specified -->
   <MozParam name="channel" condition="purpose" purpose="" value="none"/>
   <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
   <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
 </Url>
 <SearchForm>http://www.google.com/</SearchForm>
--- a/toolkit/components/search/tests/xpcshell/data/search.json
+++ b/toolkit/components/search/tests/xpcshell/data/search.json
@@ -19,16 +19,17 @@
               "rels": [
               ],
               "type": "application/x-suggestions+json",
               "params": [
               ]
             },
             {
               "template": "http://www.google.com/search",
+              "resultDomain": "google.com",
               "rels": [
               ],
               "params": [
                 {
                   "name": "q",
                   "value": "{searchTerms}"
                 },
                 {
@@ -59,16 +60,17 @@
                   "name": "channel",
                   "value": "rcs",
                   "purpose": "contextmenu"
                 }
               ]
             },
             {
               "template": "http://www.google.com/search",
+              "resultDomain": "purpose.google.com",
               "rels": [
               ],
               "type": "application/x-moz-default-purpose",
               "params": [
                 {
                   "name": "q",
                   "value": "{searchTerms}"
                 },
--- a/toolkit/components/search/tests/xpcshell/head_search.js
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -1,16 +1,16 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
-
+Components.utils.import("resource://gre/modules/Promise.jsm");
 Components.utils.import("resource://testing-common/AppInfo.jsm");
 
 const BROWSER_SEARCH_PREF = "browser.search.";
 const NS_APP_SEARCH_DIR = "SrchPlugns";
 
 const MODE_RDONLY = FileUtils.MODE_RDONLY;
 const MODE_WRONLY = FileUtils.MODE_WRONLY;
 const MODE_CREATE = FileUtils.MODE_CREATE;
--- a/toolkit/components/search/tests/xpcshell/test_json_cache.js
+++ b/toolkit/components/search/tests/xpcshell/test_json_cache.js
@@ -194,16 +194,17 @@ let EXPECTED_ENGINE = {
           template: "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox" +
                       "&hl={moz:locale}&q={searchTerms}",
           params: "",
         },
         {
           type: "text/html",
           method: "GET",
           template: "http://www.google.com/search",
+          resultDomain: "google.com",
           params: [
             {
               "name": "q",
               "value": "{searchTerms}",
               "purpose": undefined,
             },
             {
               "name": "ie",
@@ -245,16 +246,17 @@ let EXPECTED_ENGINE = {
               "mozparam": true,
             },
           },
         },
         {
           type: "application/x-moz-default-purpose",
           method: "GET",
           template: "http://www.google.com/search",
+          resultDomain: "purpose.google.com",
           params: [
             {
               "name": "q",
               "value": "{searchTerms}",
               "purpose": undefined,
             },
             {
               "name": "client",
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_resultDomain.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ *    http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getResultDomain API.
+ */
+
+"use strict";
+
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://testing-common/httpd.js");
+
+let waitForEngines = new Set([ "Test search engine",
+                               "A second test engine",
+                               "bacon" ]);
+
+function promiseEnginesAdded() {
+  let deferred = Promise.defer();
+
+  let observe = function observe(aSubject, aTopic, aData) {
+    let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
+    do_print("Observer: " + aData + " for " + engine.name);
+    if (aData != "engine-added") {
+      return;
+    }
+    waitForEngines.delete(engine.name);
+    if (waitForEngines.size > 0) {
+      return;
+    }
+
+    let engine1 = Services.search.getEngineByName("Test search engine");
+    do_check_eq(engine1.getResultDomain(), "google.com");
+    do_check_eq(engine1.getResultDomain("text/html"), "google.com");
+    do_check_eq(engine1.getResultDomain("application/x-moz-default-purpose"),
+                "purpose.google.com");
+    do_check_eq(engine1.getResultDomain("fake-response-type"), "");
+    let engine2 = Services.search.getEngineByName("A second test engine");
+    do_check_eq(engine2.getResultDomain(), "duckduckgo.com");
+    let engine3 = Services.search.getEngineByName("bacon");
+    do_check_eq(engine3.getResultDomain(), "bacon.moz");
+    deferred.resolve();
+  };
+
+  Services.obs.addObserver(observe, "browser-search-engine-modified", false);
+  do_register_cleanup(function cleanup() {
+    Services.obs.removeObserver(observe, "browser-search-engine-modified");
+  });
+
+  return deferred.promise;
+}
+
+function run_test() {
+  removeMetadata();
+  updateAppInfo();
+
+  run_next_test();
+}
+
+add_task(function* check_resultDomain() {
+  let httpServer = new HttpServer();
+  httpServer.start(-1);
+  httpServer.registerDirectory("/", do_get_cwd());
+  let baseUrl = "http://localhost:" + httpServer.identity.primaryPort;
+  do_register_cleanup(function cleanup() {
+    httpServer.stop(function() {});
+  });
+
+  let promise = promiseEnginesAdded();
+  Services.search.addEngine(baseUrl + "/data/engine.xml",
+                            Ci.nsISearchEngine.DATA_XML,
+                            null, false);
+  Services.search.addEngine(baseUrl + "/data/engine2.xml",
+                            Ci.nsISearchEngine.DATA_XML,
+                            null, false);
+  Services.search.addEngineWithDetails("bacon", "", "bacon", "Search Bacon",
+                                       "GET", "http://www.bacon.moz/?search={searchTerms}");
+  yield promise;
+});
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -26,13 +26,14 @@ support-files =
 [test_nodb_pluschanges.js]
 [test_save_sorted_engines.js]
 [test_purpose.js]
 [test_defaultEngine.js]
 [test_prefSync.js]
 [test_notifications.js]
 [test_addEngine_callback.js]
 [test_multipleIcons.js]
+[test_resultDomain.js]
 [test_serialize_file.js]
 [test_async.js]
 [test_sync.js]
 [test_sync_fallback.js]
 [test_sync_delay_fallback.js]
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -2840,16 +2840,25 @@
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 50,
     "high": 2000,
     "n_buckets": 10,
     "extended_statistics_ok": true,
     "description": "PLACES: Time to convert and write bookmarks.html"
   },
+  "PLACES_BACKUPS_HASHING_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 50,
+    "high": 2000,
+    "n_buckets": 10,
+    "extended_statistics_ok": true,
+    "description": "PLACES: Time to calculate the md5 hash for a backup"
+  },
   "FENNEC_FAVICONS_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "2000",
     "n_buckets": 10,
     "cpp_guard": "ANDROID",
     "extended_statistics_ok": true,
     "description": "FENNEC: (Places) Number of favicons stored"