Merge mozilla-central to mozilla-beta. a=merge l10n=merge
authorCosmin Sabou <csabou@mozilla.com>
Tue, 14 May 2019 18:17:44 +0300
changeset 535603 5fb7fcd568d6fbc0b205f28d7087a5ad3156456a
parent 535464 c1c7a9966c5a313a27af9c001882b168bf6142b1 (current diff)
parent 535602 30817917ace8ef05f5badd1522f7f1adac64d1e5 (diff)
child 535604 7e7b48b13b9aec4059b1b6298b9e94b37cc0d44e
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone68.0
Merge mozilla-central to mozilla-beta. a=merge l10n=merge
browser/base/content/test/general/browser_bug435325.js
browser/extensions/screenshots/_locales/en_GB/messages.json
devtools/shared/specs/script.js
mobile/android/app/src/main/res/drawable-hdpi/tracking_protection_toolbar_illustration.png
mobile/android/app/src/main/res/drawable-xhdpi/tracking_protection_toolbar_illustration.png
mobile/android/app/src/main/res/drawable-xxhdpi/tracking_protection_toolbar_illustration.png
mobile/android/app/src/main/res/layout/tracking_protection_prompt.xml
mobile/android/base/java/org/mozilla/gecko/trackingprotection/TrackingProtectionPrompt.java
testing/config/tooltool-manifests/linux32/jimdb-arm-pie.manifest
testing/config/tooltool-manifests/linux32/jimdb-arm.manifest
testing/config/tooltool-manifests/linux32/jimdb-x86-pie.manifest
testing/config/tooltool-manifests/linux32/jimdb-x86.manifest
testing/config/tooltool-manifests/linux64/jimdb-arm-pie.manifest
testing/config/tooltool-manifests/linux64/jimdb-arm.manifest
testing/config/tooltool-manifests/linux64/jimdb-x86-pie.manifest
testing/config/tooltool-manifests/linux64/jimdb-x86.manifest
testing/config/tooltool-manifests/macosx64/jimdb-arm-pie.manifest
testing/config/tooltool-manifests/macosx64/jimdb-arm.manifest
testing/config/tooltool-manifests/macosx64/jimdb-x86-pie.manifest
testing/config/tooltool-manifests/macosx64/jimdb-x86.manifest
third_party/rust/mio/ci/run-ios.sh
third_party/rust/rand/appveyor.yml
third_party/rust/rand/benches/bench.rs
third_party/rust/rand/benches/distributions/exponential.rs
third_party/rust/rand/benches/distributions/gamma.rs
third_party/rust/rand/benches/distributions/mod.rs
third_party/rust/rand/benches/distributions/normal.rs
third_party/rust/rand/src/distributions/range.rs
third_party/rust/rand/src/jitter.rs
third_party/rust/rand/src/os.rs
third_party/rust/rand/src/prng/chacha.rs
third_party/rust/rand/src/prng/isaac.rs
third_party/rust/rand/src/prng/isaac64.rs
third_party/rust/rand/src/prng/xorshift.rs
third_party/rust/rand/src/rand_impls.rs
third_party/rust/rand/src/read.rs
third_party/rust/rand/src/reseeding.rs
third_party/rust/rand/src/seq.rs
third_party/rust/rand/utils/ziggurat_tables.py
third_party/rust/sha1/LICENSE
toolkit/mozapps/extensions/content/updateinfo.xsl
--- a/accessible/tests/mochitest/states/test_textbox.xul
+++ b/accessible/tests/mochitest/states/test_textbox.xul
@@ -107,14 +107,14 @@
 
   <vbox flex="1">
     <textbox id="textbox"/>
     <textbox id="password" type="password"/>
 
     <textbox id="readonly_textbox" readonly="true"/>
     <textbox id="disabled_textbox" disabled="true"/>
 
-    <textbox id="searchbox" flex="1" type="search" results="historyTree"/>
+    <textbox id="searchbox" flex="1" is="search-textbox" results="historyTree"/>
     <textbox id="searchfield" placeholder="Search all add-ons"
-             type="search" searchbutton="true"/>
+             is="search-textbox" searchbutton="true"/>
   </vbox>
   </hbox>
 </window>
--- a/accessible/tests/mochitest/tree/test_txtctrl.xul
+++ b/accessible/tests/mochitest/tree/test_txtctrl.xul
@@ -37,35 +37,32 @@
       // default textbox
       testAccessibleTree("txc", accTree);
 
       //////////////////////////////////////////////////////////////////////////
       // search textbox
       accTree =
         { SECTION: [
           { ENTRY: [ { TEXT_LEAF: [] } ] },
-          { MENUPOPUP: [] }
         ] };
       testAccessibleTree("txc_search", accTree);
 
       //////////////////////////////////////////////////////////////////////////
       // search textbox with search button
 
       if (MAC) {
         accTree =
           { SECTION: [
             { ENTRY: [ { TEXT_LEAF: [] } ] },
-            { MENUPOPUP: [] }
           ] };
       } else {
         accTree =
           { SECTION: [
             { ENTRY: [ { TEXT_LEAF: [] } ] },
             { PUSHBUTTON: [] },
-            { MENUPOPUP: [] }
           ] };
       }
 
       testAccessibleTree("txc_search_searchbutton", accTree);
 
       //////////////////////////////////////////////////////////////////////////
       // number textbox
 
@@ -151,17 +148,17 @@
       <div id="content" style="display: none">
       </div>
       <pre id="test">
       </pre>
     </body>
 
     <vbox flex="1">
       <textbox id="txc" value="hello"/>
-      <textbox id="txc_search" type="search" value="hello"/>
-      <textbox id="txc_search_searchbutton" searchbutton="true" type="search" value="hello"/>
+      <textbox id="txc_search" is="search-textbox" value="hello"/>
+      <textbox id="txc_search_searchbutton" searchbutton="true" is="search-textbox" value="hello"/>
       <textbox id="txc_number" type="number" value="44"/>
       <textbox id="txc_password" type="password" value="hello"/>
       <textbox id="txc_autocomplete" type="autocomplete" value="hello"/>
     </vbox>
   </hbox>
 
 </window>
--- a/browser/actors/NetErrorChild.jsm
+++ b/browser/actors/NetErrorChild.jsm
@@ -270,39 +270,29 @@ class NetErrorChild extends ActorChild {
         technicalInfo.textContent = "";
         let brandName = gBrandBundle.GetStringFromName("brandShortName");
         msg = gPipNSSBundle.formatStringFromName("certErrorMismatch3", [brandName, hostString], 2) + " ";
         technicalInfo.append(msg + "\n");
       }
     }
 
     if (input.data.isNotValidAtThisTime) {
-      let nowTime = new Date().getTime() * 1000;
-      let msg = "";
-      let notAfterLocalTime = formatter.format(new Date(input.data.validity.notAfterLocalTime));
-      if (input.data.validity.notBefore) {
-        let notBeforeLocalTime = formatter.format(new Date(input.data.validity.notBeforeLocalTime));
-        if (nowTime > input.data.validity.notAfter) {
-          technicalInfo.textContent = "";
-          msg += gPipNSSBundle.formatStringFromName("certErrorExpiredNow3",
-                                                  [hostString, notAfterLocalTime], 2);
-          msg += "\n";
-        } else {
-          technicalInfo.textContent = "";
-          msg += gPipNSSBundle.formatStringFromName("certErrorNotYetValidNow3",
-                                                    [hostString, notBeforeLocalTime], 2);
-          msg += "\n";
-         }
-        } else {
-          // If something goes wrong, we assume the cert expired.
-          technicalInfo.textContent = "";
-          msg += gPipNSSBundle.formatStringFromName("certErrorExpiredNow3",
-                                                    [hostString, notAfterLocalTime], 2);
-          msg += "\n";
+      let msg;
+      if (input.data.validity.notBefore && (Date.now() < input.data.validity.notAfter)) {
+        let notBeforeLocalTime = formatter.format(new Date(input.data.validity.notBefore));
+        msg = gPipNSSBundle.formatStringFromName("certErrorNotYetValidNow3",
+                                                 [hostString, notBeforeLocalTime], 2);
+      } else {
+        let notAfterLocalTime = formatter.format(new Date(input.data.validity.notAfter));
+        msg = gPipNSSBundle.formatStringFromName("certErrorExpiredNow3",
+                                                 [hostString, notAfterLocalTime], 2);
       }
+      msg += "\n";
+
+      technicalInfo.textContent = "";
       technicalInfo.append(msg);
     }
     technicalInfo.append("\n");
 
     // Add link to certificate and error message.
     let linkPrefix = gPipNSSBundle.GetStringFromName("certErrorCodePrefix3");
     let detailLink = doc.createElement("a");
     detailLink.append(input.data.codeString);
@@ -815,17 +805,18 @@ class NetErrorChild extends ActorChild {
   onClick(event) {
     let {documentURI} = event.target.ownerDocument;
 
     let elmId = event.originalTarget.getAttribute("id");
     if (elmId == "returnButton") {
       this.mm.sendAsyncMessage("Browser:SSLErrorGoBack", {});
       return;
     }
-    if (elmId != "errorTryAgain" || !/e=netOffline/.test(documentURI)) {
+
+    if (!event.originalTarget.classList.contains("try-again") || !/e=netOffline/.test(documentURI)) {
       return;
     }
     // browser front end will handle clearing offline mode and refreshing
     // the page *if* we're in offline mode now. Otherwise let the error page
     // handle the click.
     if (Services.io.offline) {
       event.preventDefault();
       this.mm.sendAsyncMessage("Browser:EnableOnlineMode", {});
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<blocklist lastupdate="1557222511299" xmlns="http://www.mozilla.org/2006/addons-blocklist">
+<blocklist lastupdate="1557497630665" xmlns="http://www.mozilla.org/2006/addons-blocklist">
   <emItems>
     <emItem blockID="i334" id="{0F827075-B026-42F3-885D-98981EE7B1AE}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="i1211" id="flvto@hotger.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
@@ -2899,16 +2899,48 @@
     <emItem blockID="d7ca07b4-9c97-4f49-a304-117c874ff073" id="artur.dubovoy@gmail.com">
       <prefs/>
       <versionRange minVersion="16.3.5" maxVersion="16.3.9" severity="3"/>
     </emItem>
     <emItem blockID="b62c9ee1-d66f-4964-906e-2a9b07e3fdc1" id="/^((adsmin@vietbacsecurity\.com)|(\{efdefbd4-5c30-42c3-ad2b-4c49082ec4cd\})|(\{63d83b36-a85c-4b51-8f68-8eb6c0ea6922\})|(\{4613a1ed-6cb1-410b-a8b1-3f81f73b6e00\})|(\{90b1aef7-7a52-4649-b5ca-91b5e81b5eab\})|(\{d6e2e76d-edff-416b-8c04-53052ff9fec7\})|(\{43af2e0f-b5ce-409b-9ee6-5360785c9b08\})|(\{e45fa96d-8b74-4666-86de-3bbfb774a74f\})|(\{4f8332b6-6167-4b7f-a1f9-61d8eb89b102\})|(cpcnbnofbhmpimepokdpmoomejafefhb@chrome-store-foxified-14654081)|(developios89@gmail\.com)|(\{d82da356-1fa8-4550-958a-bd2472972314\})|(\{1dfbd1c3-a8ca-4eb3-8747-d30bfd20ecd5\})|(\{6f9fa22a-128f-4d1b-8ef5-d20a44d24245\})|(\{5f6af572-35c1-44d7-9d0f-dffbb62fcafe\})|(developper@avast\.com)|(\{886a6486-37b3-4bcd-891b-fd0e335e7b1a\})|(\{886a6486-37b3-4bcd-891b-fd0e355e7b1a\})|(\{d1cd26ff-fde7-46a4-85cc-48e3bb7e9e8d\})|(\{ae11d5cc-8efb-43a0-89bf-e5a779b4fa40\})|(\{aca140ce-8249-4e6e-8e2c-cd5b1c987441\})|(\{f68b2ca7-0d2c-44cc-afc8-a606a896c467\})|(\{321db3c3-8cfd-49f1-99de-fcdc3485b379\}))$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
+    <emItem blockID="2feeb46a-6784-4c6e-8c07-e120bec00b14" id="/^((\{d389cdfe-843e-44cb-b127-441492e46e63\})|(\{1340c760-3f4c-4428-b2c0-88821a84de2b\})|(\{38524a16-a73d-4a8f-8111-f9347bb5266c\}))$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+    <emItem blockID="b9686c72-1902-4868-88d1-6587fd24a57c" id="/^((\{c8d0fea0-d7b7-4f6f-b9bc-9df6722d9d18\})|(\{bed8e1f2-b00b-44e3-8cf0-5335080d0003\}))$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+    <emItem blockID="312e30b0-0b4c-4a43-8f6c-8b8447a20f6a" id="{5308dcd8-f3c7-4b85-ad66-54a120243594}">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+    <emItem blockID="04a300c2-04fc-401e-a428-c7c887bf2bff" id="/^((\{4e84c504-10e8-4e75-8885-dcc0c90999b9\})|(\{8ce99d6d-8d0d-4420-bd17-c303bd8a763e\})|(\{16de314a-56cd-4175-9baf-bbe0b09dfed3\}))$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+    <emItem blockID="12a0c69f-e755-428b-97dc-229bccb8a5b0" id="{2b10c1c8-a11f-4bad-fe9c-1c11e82cac42}">
+      <prefs/>
+      <versionRange minVersion="0.9.5.11" maxVersion="0.9.5.14" severity="1"/>
+    </emItem>
+    <emItem blockID="f0fc8d21-d0ec-4285-82d7-d482dae772bc" id="{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}">
+      <prefs/>
+      <versionRange minVersion="3.2" maxVersion="3.5.1" severity="1"/>
+    </emItem>
+    <emItem blockID="8ff19ad3-e4e0-40e3-8f02-fd80d18f63b5" id="jid1-NIfFY2CA8fy1tg@jetpack">
+      <prefs/>
+      <versionRange minVersion="3.19.0" maxVersion="3.28.0" severity="1"/>
+    </emItem>
+    <emItem blockID="fee4b92e-146b-437d-9cc0-95cfc800f0e0" id="/^((\{da61a3e5-5a98-4c47-ae6c-f4db738f1133\})|(\{b0e13c2b-c1cd-426b-bed9-905bf9557fbf\})|(\{328c22c5-5f1c-4eb7-95a3-148fd4ad429d\})|(\{f6cca5fb-5aa1-471f-88f3-e2ffa87281ef\})|(\{d342bf37-554e-41c9-b67b-72769e59b82b\})|(\{03ec69b5-3e8e-4bb8-8eda-28f12c54bff8\})|(\{a8c876cb-af13-4ad9-9a86-fc3c0722b48c\})|(\{56136c32-0159-4368-9d28-c1b8b1515c89\})|(\{79bf4660-9729-444b-ae03-6c8005869611\})|(\{aa7fdaa5-d888-47e2-b27b-4fa4b3225339\})|(\{31e0d180-52b1-4c1d-8f84-7e625715edc4\})|(\{f7d20549-e5ee-4045-9e8f-9705bb10c104\})|(\{303abacb-760b-43c3-9640-5b456d92db78\})|(\{debabd67-2e0a-485e-8213-ac081065a027\})|(\{971e739b-c528-41b6-a60c-48fc3cdb52d9\})|(\{ffb3a485-2723-4a88-b3ad-8b29773759c4\})|(\{b076177a-a5c4-4652-9f6d-953f89f9a81a\})|(\{66210cb7-6352-45d5-9d22-ad7a0fb5e247\})|(\{8053ad7b-5129-4c74-ade9-8166c38e8636\})|(\{1a435c36-133e-4163-ac71-8701a147880c\})|(\{8c40c6df-7c9d-4876-bcbe-0621734aba45\})|(\{40e1e7d9-ae29-4aec-9465-5e0d49859583\})|(\{74eab03b-35cd-4950-b436-7afce3876e58\})|(\{95839c11-63a7-4b2b-b3d3-eee9d2c5c42d\})|(\{bfaa03c3-744e-48eb-8fb6-4ad61791d4d8\})|(\{f123e726-9396-4899-822a-172b8bcb2c5f\})|(\{157e255b-2053-4140-b95c-ff003b62bf17\})|(\{3e49a17b-b58e-417b-9ebb-a7e8c2317893\})|(\{4df1d536-e30f-4344-bee6-6ef2def890c2\})|(\{f33ce070-63f4-4d2b-823e-d52fc7a30ba7\})|(\{2003e2a5-e848-4fc5-8e7d-3af1efe4f992\})|(\{ff2157da-6981-40b6-aa60-d8125e73868e\})|(\{d89fa1e5-c9d4-4104-ad8e-00b39e5c6d15\})|(\{66e45d14-550f-4489-98c6-8a0caed33375\})|(\{86e6d45f-1dfe-4e53-bf52-22bf65b9ae6d\})|(\{e71407fe-e1ed-4755-af8f-dd64a952ce1a\})|(\{b67b3615-d8fe-4961-a41e-391864afde2d\})|(\{5785789b-ccba-44a1-9018-1135b56bd37f\})|(\{6dfb93d1-2add-471c-bbbc-b6164b4c1d94\}))$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
   </emItems>
   <pluginItems>
     <pluginItem blockID="p332">
       <match exp="libflashplayer\.so" name="filename"/>
       <match exp="^Shockwave Flash 11.(0|1) r[0-9]{1,3}$" name="description"/>
       <infoURL>https://get.adobe.com/flashplayer/</infoURL>
       <versionRange severity="0" vulnerabilitystatus="1">
         <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
--- a/browser/base/content/aboutNetError.js
+++ b/browser/base/content/aboutNetError.js
@@ -68,17 +68,17 @@ function showCertificateErrorReporting()
 function showPrefChangeContainer() {
   const panel = document.getElementById("prefChangeContainer");
   panel.style.display = "block";
   document.getElementById("netErrorButtonContainer").style.display = "none";
   document.getElementById("prefResetButton").addEventListener("click", function resetPreferences(e) {
     const event = new CustomEvent("AboutNetErrorResetPreferences", {bubbles: true});
     document.dispatchEvent(event);
   });
-  addAutofocus("prefResetButton", "beforeend");
+  addAutofocus("#prefResetButton", "beforeend");
 }
 
 function setupAdvancedButton() {
   // Get the hostname and add it to the panel
   var panel = document.getElementById("badCertAdvancedPanel");
   for (var span of panel.querySelectorAll("span.hostname")) {
     span.textContent = document.location.hostname;
   }
@@ -202,17 +202,17 @@ function initPage() {
     initPageCaptivePortal();
     return;
   }
   if (gIsCertError) {
     initPageCertError();
     updateContainerPosition();
     return;
   }
-  addAutofocus("errorTryAgain");
+  addAutofocus("#netErrorButtonContainer > .try-again");
 
   document.body.classList.add("neterror");
 
   var ld = document.getElementById("errorLongDesc");
   if (ld) {
     // eslint-disable-next-line no-unsanitized/property
     ld.innerHTML = errDesc.innerHTML;
   }
@@ -278,17 +278,17 @@ function initPage() {
 
   var event = new CustomEvent("AboutNetErrorLoad", {bubbles: true});
   document.dispatchEvent(event);
 
   if (err == "inadequateSecurityError" || err == "blockedByPolicy") {
     // Remove the "Try again" button from pages that don't need it.
     // For HTTP/2 inadequate security or pages blocked by policy, trying
     // again won't help.
-    document.getElementById("errorTryAgain").style.display = "none";
+    document.getElementById("netErrorButtonContainer").style.display = "none";
 
     var container = document.getElementById("errorLongDesc");
     for (var span of container.querySelectorAll("span.hostname")) {
       span.textContent = document.location.hostname;
     }
   }
 }
 
@@ -313,33 +313,33 @@ function updateContainerPosition() {
 
 function initPageCaptivePortal() {
   document.body.className = "captiveportal";
   document.getElementById("openPortalLoginPageButton")
           .addEventListener("click", () => {
     RPMSendAsyncMessage("Browser:OpenCaptivePortalPage");
   });
 
-  addAutofocus("openPortalLoginPageButton");
+  addAutofocus("#openPortalLoginPageButton");
   setupAdvancedButton();
 
   // When the portal is freed, an event is sent by the parent process
   // that we can pick up and attempt to reload the original page.
   RPMAddMessageListener("AboutNetErrorCaptivePortalFreed", () => {
     document.location.reload();
   });
 }
 
 function initPageCertError() {
   document.body.classList.add("certerror");
   for (let host of document.querySelectorAll(".hostname")) {
     host.textContent = document.location.hostname;
   }
 
-  addAutofocus("returnButton");
+  addAutofocus("#returnButton");
   setupAdvancedButton();
 
   document.getElementById("learnMoreContainer").style.display = "block";
 
   let checkbox = document.getElementById("automaticallyReportInFuture");
   checkbox.addEventListener("change", function({target: {checked}}) {
     document.dispatchEvent(new CustomEvent("AboutNetErrorSetAutomatic", {
       detail: checked,
@@ -366,32 +366,28 @@ function initPageCertError() {
 }
 
 /* Only do autofocus if we're the toplevel frame; otherwise we
    don't want to call attention to ourselves!  The key part is
    that autofocus happens on insertion into the tree, so we
    can remove the button, add @autofocus, and reinsert the
    button.
 */
-function addAutofocus(buttonId, position = "afterbegin") {
+function addAutofocus(selector, position = "afterbegin") {
   if (window.top == window) {
-      var button = document.getElementById(buttonId);
+      var button = document.querySelector(selector);
       var parent = button.parentNode;
       button.remove();
       button.setAttribute("autofocus", "true");
       parent.insertAdjacentElement(position, button);
   }
 }
 
-let errorTryAgain = document.getElementById("errorTryAgain");
-errorTryAgain.addEventListener("click", function() {
-  retryThis(this);
-});
-
-let advancedPanelErrorTryAgain = document.getElementById("advancedPanelErrorTryAgain");
-advancedPanelErrorTryAgain.addEventListener("click", function() {
-  retryThis(this);
-});
+for (let button of document.querySelectorAll(".try-again")) {
+  button.addEventListener("click", function() {
+    retryThis(this);
+  });
+}
 
 // Note: It is important to run the script this way, instead of using
 // an onload handler. This is because error pages are loaded as
 // LOAD_BACKGROUND, which means that onload handlers will not be executed.
 initPage();
--- a/browser/base/content/aboutNetError.xhtml
+++ b/browser/base/content/aboutNetError.xhtml
@@ -191,32 +191,32 @@
         <div id="prefChangeContainer" class="button-container">
           <p>&prefReset.longDesc;</p>
           <button id="prefResetButton" class="primary">&prefReset.label;</button>
         </div>
 
         <div id="certErrorAndCaptivePortalButtonContainer" class="button-container">
           <button id="returnButton" class="primary" data-telemetry-id="return_button_top">&returnToPreviousPage1.label;</button>
           <button id="openPortalLoginPageButton" class="primary">&openPortalLoginPage.label2;</button>
-          <button id="errorTryAgain" class="primary">&retry.label;</button>
+          <button class="primary try-again">&retry.label;</button>
           <button id="advancedButton" data-telemetry-id="advanced_button">&advanced2.label;</button>
         </div>
       </div>
 
       <div id="netErrorButtonContainer" class="button-container">
-        <button id="errorTryAgain" class="primary">&retry.label;</button>
+        <button class="primary try-again">&retry.label;</button>
       </div>
 
       <div id="advancedPanelContainer">
         <div id="badCertAdvancedPanel" class="advanced-panel">
           <p id="badCertTechnicalInfo"/>
           <a id="viewCertificate" href="javascript:void(0)">&viewCertificate.label;</a>
           <div id="advancedPanelButtonContainer" class="button-container">
             <button id="advancedPanelReturnButton" class="primary" data-telemetry-id="return_button_adv">&returnToPreviousPage1.label;</button>
-            <button id="advancedPanelErrorTryAgain" class="primary">&retry.label;</button>
+            <button class="primary try-again">&retry.label;</button>
             <div class="exceptionDialogButtonContainer">
               <button id="exceptionDialogButton" data-telemetry-id="exception_button">&securityOverride.exceptionButton1Label;</button>
             </div>
           </div>
         </div>
 
         <div id="certificateErrorReporting">
             <p class="toggle-container-with-text">
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3233,20 +3233,18 @@ var BrowserOnClick = {
         goBackFromErrorPage();
         break;
 
       case "advancedButton":
         securityInfo = getSecurityInfo(securityInfoAsString);
         let errorInfo = getDetailedCertErrorInfo(location,
                                                  securityInfo);
         let validityInfo = {
-          notAfter: securityInfo.serverCert.validity.notAfter,
-          notBefore: securityInfo.serverCert.validity.notBefore,
-          notAfterLocalTime: securityInfo.serverCert.validity.notAfterLocalTime,
-          notBeforeLocalTime: securityInfo.serverCert.validity.notBeforeLocalTime,
+          notAfter: securityInfo.serverCert.validity.notAfter / 1000,
+          notBefore: securityInfo.serverCert.validity.notBefore / 1000,
         };
         browser.messageManager.sendAsyncMessage("CertErrorDetails", {
             code: securityInfo.errorCode,
             info: errorInfo,
             codeString: securityInfo.errorCodeString,
             certIsUntrusted: securityInfo.isUntrusted,
             certSubjectAltNames: securityInfo.serverCert.subjectAltNames,
             validity: validityInfo,
--- a/browser/base/content/test/about/browser.ini
+++ b/browser/base/content/test/about/browser.ini
@@ -20,8 +20,10 @@ prefs =
 [browser_aboutHome_search_searchbar.js]
 [browser_aboutHome_search_suggestion.js]
 skip-if = os == "mac" || (os == "linux" && (!debug || bits == 64)) || (os == 'win' && os_version == '10.0' && bits == 64 && !debug) # Bug 1399648, bug 1402502
 [browser_aboutHome_search_telemetry.js]
 [browser_aboutNetError.js]
 [browser_aboutStopReload.js]
 [browser_aboutSupport.js]
 [browser_aboutSupport_newtab_security_state.js]
+[browser_bug435325.js]
+skip-if = verify && !debug && os == 'mac'
rename from browser/base/content/test/general/browser_bug435325.js
rename to browser/base/content/test/about/browser_bug435325.js
--- a/browser/base/content/test/general/browser_bug435325.js
+++ b/browser/base/content/test/about/browser_bug435325.js
@@ -25,17 +25,17 @@ add_task(async function checkSwitchPageT
     // Re-enable the proxy so example.com is resolved to localhost, rather than
     // the actual example.com.
     await SpecialPowers.pushPrefEnv({"set": [["network.proxy.type", proxyPrefValue]]});
     let changeObserved = TestUtils.topicObserved("network:offline-status-changed");
 
     // Click on the 'Try again' button.
     await ContentTask.spawn(browser, null, async function() {
       ok(content.document.documentURI.startsWith("about:neterror?e=netOffline"), "Should be showing error page");
-      content.document.getElementById("errorTryAgain").click();
+      content.document.querySelector("#netErrorButtonContainer > .try-again").click();
     });
 
     await changeObserved;
     ok(!Services.io.offline, "After clicking the 'Try Again' button, we're back online.");
   });
 });
 
 registerCleanupFunction(function() {
--- a/browser/base/content/test/forms/browser_selectpopup.js
+++ b/browser/base/content/test/forms/browser_selectpopup.js
@@ -635,17 +635,17 @@ async function performSelectSearchTests(
 
     select.options[1].selected = true;
     select.focus();
   });
 
   let selectPopup = win.document.getElementById("ContentSelectDropdown").menupopup;
   await openSelectPopup(selectPopup, false, "select", win);
 
-  let searchElement = selectPopup.querySelector("textbox");
+  let searchElement = selectPopup.querySelector(".contentSelectDropdown-searchbox");
   searchElement.focus();
 
   EventUtils.synthesizeKey("O", {}, win);
   is(selectPopup.children[2].hidden, false, "First option should be visible");
   is(selectPopup.children[3].hidden, false, "Second option should be visible");
 
   EventUtils.synthesizeKey("3", {}, win);
   is(selectPopup.children[2].hidden, true, "First option should be hidden");
--- a/browser/base/content/test/forms/browser_selectpopup_searchfocus.js
+++ b/browser/base/content/test/forms/browser_selectpopup_searchfocus.js
@@ -23,20 +23,19 @@ add_task(async function test_focus_on_se
 
   let menulist = document.getElementById("ContentSelectDropdown");
   let selectPopup = menulist.menupopup;
 
   let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
   await BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mousedown" }, gBrowser.selectedBrowser);
   await popupShownPromise;
 
-  let searchInput = selectPopup.querySelector("textbox[type='search']");
+  let searchInput = selectPopup.querySelector(".contentSelectDropdown-searchbox");
   searchInput.scrollIntoView();
-  let searchFocused = BrowserTestUtils.waitForEvent(searchInput, "focus");
+  let searchFocused = BrowserTestUtils.waitForEvent(searchInput, "focus", true);
   await EventUtils.synthesizeMouseAtCenter(searchInput, {}, window);
   await searchFocused;
 
   is(selectPopup.state, "open", "select popup should still be open after clicking on the search field");
 
   await hideSelectPopup(selectPopup, "escape");
   BrowserTestUtils.removeTab(tab);
 });
-
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -101,19 +101,16 @@ skip-if = true # bug 428712
 [browser_bug424101.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug427559.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug431826.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug432599.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_bug435325.js]
-skip-if = verify && !debug && os == 'mac'
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug441778.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug455852.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug462289.js]
 skip-if = toolkit == "cocoa"
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug462673.js]
--- a/browser/base/content/test/siteIdentity/test_no_mcb_for_loopback.html
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_for_loopback.html
@@ -1,14 +1,14 @@
 <!-- See browser_no_mcb_for_localhost.js -->
 <!DOCTYPE HTML>
 <html>
   <head>
     <meta charset="utf8">
-    <title>Bug 903966</title>
+    <title>Bug 903966, Bug 1402530</title>
   </head>
 
   <style>
     @font-face {
       font-family: "Font-IPv4";
       src: url("http://127.0.0.1:8/test.ttf");
     }
 
@@ -27,24 +27,29 @@
   </style>
 
   <body>
     <div id="ip-v4">test</div>
     <div id="ip-v6">test</div>
 
     <img src="http://127.0.0.1:8/test.png">
     <img src="http://[::1]:8/test.png">
+    <img src="http://localhost:8/test.png">
 
     <iframe src="http://127.0.0.1:8/test.html"></iframe>
     <iframe src="http://[::1]:8/test.html"></iframe>
+    <iframe src="http://localhost:8/test.html"></iframe>
   </body>
 
   <script src="http://127.0.0.1:8/test.js"></script>
   <script src="http://[::1]:8/test.js"></script>
-
+  <script src="http://localhost:8/test.js"></script>
+  
   <link href="http://127.0.0.1:8/test.css" rel="stylesheet"></link>
   <link href="http://[::1]:8/test.css" rel="stylesheet"></link>
+  <link href="http://localhost:8/test.css" rel="stylesheet"></link>
 
   <script>
     fetch("http://127.0.0.1:8");
+    fetch("http://localhost:8");
     fetch("http://[::1]:8");
   </script>
 </html>
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -142,16 +142,18 @@ var whitelist = [
   {file: "chrome://mozapps/skin/downloads/downloadButtons.png", platforms: ["linux", "win"]},
   // Bug 1348558
   {file: "chrome://mozapps/skin/update/downloadButtons.png",
    platforms: ["linux"]},
   // Bug 1348559
   {file: "chrome://pippki/content/resetpassword.xul"},
   // Bug 1337345
   {file: "resource://gre/modules/Manifest.jsm"},
+  // Bug 1548381
+  {file: "resource://gre/modules/PasswordGenerator.jsm"},
   // Bug 1351097
   {file: "resource://gre/modules/accessibility/AccessFu.jsm"},
   // Bug 1356043
   {file: "resource://gre/modules/PerfMeasurement.jsm"},
   // Bug 1356045
   {file: "chrome://global/content/test-ipc.xul"},
   // Bug 1378173 (warning: still used by devtools)
   {file: "resource://gre/modules/Promise.jsm"},
--- a/browser/components/extensions/ext-browser.json
+++ b/browser/components/extensions/ext-browser.json
@@ -30,17 +30,17 @@
     "scopes": ["addon_parent"],
     "paths": [
       ["captivePortal"]
     ]
   },
   "chrome_settings_overrides": {
     "url": "chrome://browser/content/parent/ext-chrome-settings-overrides.js",
     "scopes": [],
-    "events": ["update", "uninstall"],
+    "events": ["update", "uninstall", "disable"],
     "schema": "chrome://browser/content/schemas/chrome_settings_overrides.json",
     "manifest": ["chrome_settings_overrides"]
   },
   "commands": {
     "url": "chrome://browser/content/parent/ext-commands.js",
     "schema": "chrome://browser/content/schemas/commands.json",
     "scopes": ["addon_parent"],
     "events": ["uninstall"],
@@ -170,16 +170,17 @@
     "paths": [
       ["sessions"]
     ]
   },
   "sidebarAction": {
     "url": "chrome://browser/content/parent/ext-sidebarAction.js",
     "schema": "chrome://browser/content/schemas/sidebar_action.json",
     "scopes": ["addon_parent"],
+    "events": ["uninstall"],
     "manifest": ["sidebar_action"],
     "paths": [
       ["sidebarAction"]
     ]
   },
   "tabs": {
     "url": "chrome://browser/content/parent/ext-tabs.js",
     "schema": "chrome://browser/content/schemas/tabs.json",
@@ -188,17 +189,17 @@
     "paths": [
       ["tabs"]
     ]
   },
   "urlOverrides": {
     "url": "chrome://browser/content/parent/ext-url-overrides.js",
     "schema": "chrome://browser/content/schemas/url_overrides.json",
     "scopes": ["addon_parent"],
-    "events": ["uninstall"],
+    "events": ["update", "uninstall", "disable"],
     "manifest": ["chrome_url_overrides"],
     "paths": [
       ["urlOverrides"]
     ]
   },
   "windows": {
     "url": "chrome://browser/content/parent/ext-windows.js",
     "schema": "chrome://browser/content/schemas/windows.json",
--- a/browser/components/extensions/parent/ext-browserAction.js
+++ b/browser/components/extensions/parent/ext-browserAction.js
@@ -110,17 +110,17 @@ this.browserAction = class extends Exten
 
   handleLocationChange(eventType, tab, fromBrowse) {
     if (fromBrowse) {
       this.tabContext.clear(tab);
       this.updateOnChange(tab);
     }
   }
 
-  onShutdown(reason) {
+  onShutdown() {
     browserActionMap.delete(this.extension);
 
     this.tabContext.shutdown();
     CustomizableUI.destroyWidget(this.id);
 
     this.clearPopup();
   }
 
--- a/browser/components/extensions/parent/ext-chrome-settings-overrides.js
+++ b/browser/components/extensions/parent/ext-chrome-settings-overrides.js
@@ -170,16 +170,23 @@ this.chrome_settings_overrides = class e
 
     let haveSearchProvider = manifest && manifest.chrome_settings_overrides &&
                              manifest.chrome_settings_overrides.search_provider;
     if (!haveSearchProvider) {
       this.removeSearchSettings(id);
     }
   }
 
+  static onDisable(id) {
+    homepagePopup.clearConfirmation(id);
+
+    chrome_settings_overrides.processDefaultSearchSetting("disable", id);
+    chrome_settings_overrides.removeEngine(id);
+  }
+
   async onManifestEntry(entryName) {
     let {extension} = this;
     let {manifest} = extension;
 
     await ExtensionSettingsStore.initialize();
 
     let homepageUrl = manifest.chrome_settings_overrides.homepage;
 
@@ -188,18 +195,17 @@ this.chrome_settings_overrides = class e
       if (extension.startupReason == "ADDON_INSTALL" ||
           extension.startupReason == "ADDON_ENABLE") {
         inControl = await ExtensionPreferencesManager.setSetting(
           extension.id, "homepage_override", homepageUrl);
       } else {
         let item = await ExtensionPreferencesManager.getSetting("homepage_override");
         inControl = item && item.id == extension.id;
       }
-      // We need to add the listener here too since onPrefsChanged won't trigger on a
-      // restart (the prefs are already set).
+
       if (inControl) {
         Services.prefs.setBoolPref(HOMEPAGE_PRIVATE_ALLOWED, extension.privateBrowsingAllowed);
         // Also set this now as an upgraded browser will need this.
         Services.prefs.setBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, true);
         if (extension.startupReason == "APP_STARTUP") {
           handleInitialHomepagePopup(extension.id, homepageUrl);
         } else {
           homepagePopup.addObserver(extension.id);
@@ -220,24 +226,16 @@ this.chrome_settings_overrides = class e
       extension.on("remove-permissions", async (ignoreEvent, permissions) => {
         if (permissions.permissions.includes("internal:privateBrowsingAllowed")) {
           let item = await ExtensionPreferencesManager.getSetting("homepage_override");
           if (item && item.id == extension.id) {
             Services.prefs.setBoolPref(HOMEPAGE_PRIVATE_ALLOWED, false);
           }
         }
       });
-
-      extension.callOnClose({
-        close: () => {
-          if (extension.shutdownReason == "ADDON_DISABLE") {
-            homepagePopup.clearConfirmation(extension.id);
-          }
-        },
-      });
     }
     if (manifest.chrome_settings_overrides.search_provider) {
       // Registering a search engine can potentially take a long while,
       // or not complete at all (when searchInitialized is never resolved),
       // so we are deliberately not awaiting the returned promise here.
       let searchStartupPromise =
         this.processSearchProviderManifestEntry().finally(() => {
           if (pendingSearchSetupTasks.get(extension.id) === searchStartupPromise) {
@@ -256,24 +254,16 @@ this.chrome_settings_overrides = class e
     let searchProvider = manifest.chrome_settings_overrides.search_provider;
     if (searchProvider.is_default) {
       await searchInitialized;
       if (!this.extension) {
         Cu.reportError(`Extension shut down before search provider was registered`);
         return;
       }
     }
-    extension.callOnClose({
-      close: () => {
-        if (extension.shutdownReason == "ADDON_DISABLE") {
-          chrome_settings_overrides.processDefaultSearchSetting("disable", extension.id);
-          chrome_settings_overrides.removeEngine(extension.id);
-        }
-      },
-    });
 
     let engineName = searchProvider.name.trim();
     if (searchProvider.is_default) {
       let engine = Services.search.getEngineByName(engineName);
       let defaultEngines = await Services.search.getDefaultEngines();
       if (engine && defaultEngines.some(defaultEngine => defaultEngine.name == engineName)) {
         // Needs to be called every time to handle reenabling, but
         // only sets default for install or enable.
--- a/browser/components/extensions/parent/ext-commands.js
+++ b/browser/components/extensions/parent/ext-commands.js
@@ -15,17 +15,17 @@ this.commands = class extends ExtensionA
       extension: this.extension,
       onCommand: (name) => this.emit("command", name),
     });
     this.extension.shortcuts = shortcuts;
     await shortcuts.loadCommands();
     await shortcuts.register();
   }
 
-  onShutdown(reason) {
+  onShutdown() {
     this.extension.shortcuts.unregister();
   }
 
   getAPI(context) {
     return {
       commands: {
         getAll: () => this.extension.shortcuts.allCommands(),
         update: (args) => this.extension.shortcuts.updateCommand(args),
--- a/browser/components/extensions/parent/ext-devtools.js
+++ b/browser/components/extensions/parent/ext-devtools.js
@@ -351,17 +351,17 @@ this.devtools = class extends ExtensionA
     if (!this.isDevToolsPageDisabled()) {
       this.pageDefinition.build();
     }
 
     DevToolsShim.on("toolbox-created", this.onToolboxCreated);
     DevToolsShim.on("toolbox-destroy", this.onToolboxDestroy);
   }
 
-  onShutdown(reason) {
+  onShutdown() {
     DevToolsShim.off("toolbox-created", this.onToolboxCreated);
     DevToolsShim.off("toolbox-destroy", this.onToolboxDestroy);
 
     // Shutdown the extension devtools_page from all existing toolboxes.
     this.pageDefinition.shutdown();
     this.pageDefinition = null;
 
     // Iterate over the existing toolboxes and unlist the devtools webextension from them.
--- a/browser/components/extensions/parent/ext-menus.js
+++ b/browser/components/extensions/parent/ext-menus.js
@@ -1095,17 +1095,17 @@ this.menusInternal = class extends Exten
     super(extension);
 
     if (!gMenuMap.size) {
       menuTracker.register();
     }
     gMenuMap.set(extension, new Map());
   }
 
-  onShutdown(reason) {
+  onShutdown() {
     let {extension} = this;
 
     if (gMenuMap.has(extension)) {
       gMenuMap.delete(extension);
       gRootItems.delete(extension);
       gShownMenuItems.delete(extension);
       gOnShownSubscribers.delete(extension);
       if (!gMenuMap.size) {
--- a/browser/components/extensions/parent/ext-omnibox.js
+++ b/browser/components/extensions/parent/ext-omnibox.js
@@ -15,17 +15,17 @@ this.omnibox = class extends ExtensionAP
       // This will throw if the keyword is already registered.
       ExtensionSearchHandler.registerKeyword(keyword, extension);
       this.keyword = keyword;
     } catch (e) {
       extension.manifestError(e.message);
     }
   }
 
-  onShutdown(reason) {
+  onShutdown() {
     ExtensionSearchHandler.unregisterKeyword(this.keyword);
   }
 
   getAPI(context) {
     let {extension} = context;
     return {
       omnibox: {
         setDefaultSuggestion: (suggestion) => {
--- a/browser/components/extensions/parent/ext-pageAction.js
+++ b/browser/components/extensions/parent/ext-pageAction.js
@@ -113,26 +113,26 @@ this.pageAction = class extends Extensio
           if (this.isShown(tab)) {
             this.updateButton(window);
           }
         }
       }
     }
   }
 
-  onShutdown(reason) {
+  onShutdown(isAppShutdown) {
     pageActionMap.delete(this.extension);
 
     this.tabContext.shutdown();
 
     // Removing the browser page action causes PageActions to forget about it
     // across app restarts, so don't remove it on app shutdown, but do remove
     // it on all other shutdowns since there's no guarantee the action will be
     // coming back.
-    if (reason != "APP_SHUTDOWN" && this.browserPageAction) {
+    if (!isAppShutdown && this.browserPageAction) {
       this.browserPageAction.remove();
       this.browserPageAction = null;
     }
   }
 
   // Returns the value of the property |prop| for the given tab, where
   // |prop| is one of "show", "title", "icon", "popup".
   getProperty(tab, prop) {
--- a/browser/components/extensions/parent/ext-sidebarAction.js
+++ b/browser/components/extensions/parent/ext-sidebarAction.js
@@ -83,52 +83,58 @@ this.sidebarAction = class extends Exten
 
     sidebarActionMap.set(extension, this);
   }
 
   onReady() {
     this.build();
   }
 
-  onShutdown(reason) {
+  onShutdown(isAppShutdown) {
     sidebarActionMap.delete(this.this);
 
     this.tabContext.shutdown();
 
     // Don't remove everything on app shutdown so session restore can handle
     // restoring open sidebars.
-    if (reason === "APP_SHUTDOWN") {
+    if (isAppShutdown) {
       return;
     }
 
     for (let window of windowTracker.browserWindows()) {
       let {document, SidebarUI} = window;
       if (SidebarUI.currentID === this.id) {
         SidebarUI.hide();
       }
-      if (SidebarUI.lastOpenedId === this.id &&
-          reason === "ADDON_UNINSTALL") {
-        SidebarUI.lastOpenedId = null;
-      }
       let menu = document.getElementById(this.menuId);
       if (menu) {
         menu.remove();
       }
       let button = document.getElementById(this.buttonId);
       if (button) {
         button.remove();
       }
       let header = document.getElementById("sidebar-switcher-target");
       header.removeEventListener("SidebarShown", this.updateHeader);
       SidebarUI.sidebars.delete(this.id);
     }
     windowTracker.removeOpenListener(this.windowOpenListener);
     windowTracker.removeCloseListener(this.windowCloseListener);
   }
 
+  static onUninstall(id) {
+    const sidebarId = `${makeWidgetId(id)}-sidebar-action`;
+    for (let window of windowTracker.browserWindows()) {
+      let {SidebarUI} = window;
+      if (SidebarUI.lastOpenedId === sidebarId) {
+        SidebarUI.lastOpenedId = null;
+      }
+    }
+  }
+
   build() {
     this.tabContext.on("tab-select", // eslint-disable-line mozilla/balanced-listeners
                        (evt, tab) => { this.updateWindow(tab.ownerGlobal); });
 
     let install = this.extension.startupReason === "ADDON_INSTALL";
     for (let window of windowTracker.browserWindows()) {
       this.updateWindow(window);
       let {SidebarUI} = window;
--- a/browser/components/extensions/parent/ext-url-overrides.js
+++ b/browser/components/extensions/parent/ext-url-overrides.js
@@ -88,67 +88,57 @@ ExtensionParent.apiManager.on("extension
       extensionId = (item && item.id) || setting.id;
       url = item && (item.value || item.initialValue);
     }
   }
   setNewTabURL(extensionId, url);
 });
 
 this.urlOverrides = class extends ExtensionAPI {
-  static onUninstall(id) {
+  static async onDisable(id) {
+    newTabPopup.clearConfirmation(id);
+    await ExtensionSettingsStore.initialize();
+    if (ExtensionSettingsStore.hasSetting(id, STORE_TYPE, NEW_TAB_SETTING_NAME)) {
+      ExtensionSettingsStore.disable(id, STORE_TYPE, NEW_TAB_SETTING_NAME);
+    }
+  }
+
+  static async onUninstall(id) {
     // TODO: This can be removed once bug 1438364 is fixed and all data is cleaned up.
     newTabPopup.clearConfirmation(id);
+
+    await ExtensionSettingsStore.initialize();
+    if (ExtensionSettingsStore.hasSetting(id, STORE_TYPE, NEW_TAB_SETTING_NAME)) {
+      ExtensionSettingsStore.removeSetting(id, STORE_TYPE, NEW_TAB_SETTING_NAME);
+    }
   }
 
-  processNewTabSetting(action) {
-    let {extension} = this;
-    ExtensionSettingsStore[action](extension.id, STORE_TYPE, NEW_TAB_SETTING_NAME);
+  static async onUpdate(id, manifest) {
+    if (!manifest.chrome_url_overrides ||
+        !manifest.chrome_url_overrides.newtab) {
+      await ExtensionSettingsStore.initialize();
+      if (ExtensionSettingsStore.hasSetting(id, STORE_TYPE, NEW_TAB_SETTING_NAME)) {
+        ExtensionSettingsStore.removeSetting(id, STORE_TYPE, NEW_TAB_SETTING_NAME);
+      }
+    }
   }
 
   async onManifestEntry(entryName) {
     let {extension} = this;
     let {manifest} = extension;
 
     await ExtensionSettingsStore.initialize();
 
     if (manifest.chrome_url_overrides.newtab) {
-      // Set up the shutdown code for the setting.
-      extension.callOnClose({
-        close: () => {
-          switch (extension.shutdownReason) {
-            case "ADDON_DISABLE":
-              this.processNewTabSetting("disable");
-              newTabPopup.clearConfirmation(extension.id);
-              break;
-
-            // We can remove the setting on upgrade or downgrade because it will be
-            // added back in when the manifest is re-read. This will cover the case
-            // where a new version of an add-on removes the manifest key.
-            case "ADDON_DOWNGRADE":
-            case "ADDON_UPGRADE":
-            case "ADDON_UNINSTALL":
-              this.processNewTabSetting("removeSetting");
-              break;
-          }
-        },
-      });
-
       let url = extension.baseURI.resolve(manifest.chrome_url_overrides.newtab);
 
       let item = await ExtensionSettingsStore.addSetting(
         extension.id, STORE_TYPE, NEW_TAB_SETTING_NAME, url,
         () => aboutNewTabService.newTabURL);
 
-      // If the extension was just re-enabled, change the setting to enabled.
-      // This is required because addSetting above is used for both add and update.
-      if (["ADDON_ENABLE", "ADDON_UPGRADE", "ADDON_DOWNGRADE"]
-          .includes(extension.startupReason)) {
-        item = ExtensionSettingsStore.enable(extension.id, STORE_TYPE, NEW_TAB_SETTING_NAME);
-      }
-
       // Set the newTabURL to the current value of the setting.
       if (item) {
         setNewTabURL(item.id, item.value || item.initialValue);
       }
 
       // We need to monitor permission change and update the preferences.
       // eslint-disable-next-line mozilla/balanced-listeners
       extension.on("add-permissions", async (ignoreEvent, permissions) => {
--- a/browser/components/extensions/test/browser/browser_ext_optionsPage_modals.js
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_modals.js
@@ -74,18 +74,23 @@ add_task(async function test_tab_options
   // be the parent of the currently selected tabbrowser's browser.
   if (optionsBrowser.isRemoteBrowser) {
     stack = optionsBrowser.parentNode;
   } else {
     stack = gBrowser.selectedBrowser.parentNode;
   }
 
   let dialogs = stack.querySelectorAll("tabmodalprompt");
+  Assert.equal(dialogs.length, 1, "Expect a tab modal opened for the about addons tab");
 
-  Assert.equal(dialogs.length, 1, "Expect a tab modal opened for the about addons tab");
+  // Verify that the expected stylesheets have been applied on the
+  // tabmodalprompt element (See Bug 1550529).
+  const tabmodalStyle = dialogs[0].ownerGlobal.getComputedStyle(dialogs[0]);
+  is(tabmodalStyle["background-color"], "rgba(26, 26, 26, 0.5)",
+     "Got the expected styles applied to the tabmodalprompt");
 
   info("Close the tab modal prompt");
   dialogs[0].querySelector(".tabmodalprompt-button0").click();
 
   await extension.awaitFinish("options-ui-modals");
 
   Assert.equal(stack.querySelectorAll("tabmodalprompt").length, 0,
                "Expect the tab modal to be closed");
--- a/browser/components/places/content/bookmarksSidebar.xul
+++ b/browser/components/places/content/bookmarksSidebar.xul
@@ -32,17 +32,17 @@
   <script src="chrome://browser/content/places/places-tree.js"/>
   <script src="chrome://global/content/editMenuOverlay.js"/>
 
 #include placesCommands.inc.xul
 #include placesContextMenu.inc.xul
 #include bookmarksHistoryTooltip.inc.xul
 
   <hbox id="sidebar-search-container" align="center">
-    <textbox id="search-box" flex="1" type="search"
+    <textbox id="search-box" flex="1" is="search-textbox"
              placeholder="&bookmarksSearch.placeholder;"
              aria-controls="bookmarks-view"
              oncommand="searchBookmarks(this.value);"/>
   </hbox>
 
   <tree id="bookmarks-view"
         class="sidebar-placesTree"
         is="places-tree"
--- a/browser/components/places/content/historySidebar.xul
+++ b/browser/components/places/content/historySidebar.xul
@@ -40,17 +40,17 @@
     <key id="key_delete2" keycode="VK_BACK" command="cmd_delete"/>
   </keyset>
 #endif
 
 #include placesContextMenu.inc.xul
 #include bookmarksHistoryTooltip.inc.xul
 
   <hbox id="sidebar-search-container">
-    <textbox id="search-box" flex="1" type="search"
+    <textbox id="search-box" flex="1" is="search-textbox"
              placeholder="&historySearch.placeholder;"
              aria-controls="historyTree"
              oncommand="searchHistory(this.value);"/>
     <button id="viewButton" style="min-width:0px !important;" type="menu"
             label="&view.label;" accesskey="&view.accesskey;" selectedsort="day"
             persist="selectedsort">
       <menupopup>
         <menuitem id="bydayandsite" label="&byDayAndSite.label;"
--- a/browser/components/places/content/places.xul
+++ b/browser/components/places/content/places.xul
@@ -328,17 +328,17 @@
         </menu>
       </menubar>
 #endif
 
       <spacer id="libraryToolbarSpacer" flex="1"/>
 
       <textbox id="searchFilter"
                flex="1"
-               type="search"
+               is="search-textbox"
                aria-controls="placeContent"
                oncommand="PlacesSearchBox.search(this.value);"
                collection="bookmarks">
       </textbox>
       <toolbarbutton id="clearDownloadsButton"
 #ifdef XP_MACOSX
                      class="tabbable"
 #endif
--- a/browser/components/pocket/content/Pocket.jsm
+++ b/browser/components/pocket/content/Pocket.jsm
@@ -8,17 +8,17 @@ var EXPORTED_SYMBOLS = ["Pocket"];
 
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
 
 var Pocket = {
   get site() { return Services.prefs.getCharPref("extensions.pocket.site"); },
-  get listURL() { return "https://" + Pocket.site + "/?src=ff_ext"; },
+  get listURL() { return "https://" + Pocket.site + "/firefox_learnmore?src=ff_library"; },
 
   openList(event) {
     let win = event.view;
     let where = win.whereToOpenLink(event);
     // Never override the current tab unless it's blank:
     if (where == "current" && !win.gBrowser.selectedTab.isEmpty) {
       where = "tab";
     }
--- a/browser/components/pocket/test/browser_pocket_library_menu_action.js
+++ b/browser/components/pocket/test/browser_pocket_library_menu_action.js
@@ -21,19 +21,19 @@ add_task(async function() {
   await libraryPromise;
 
   let pocketLibraryButton = document.getElementById("appMenu-library-pocket-button");
   ok(pocketLibraryButton, "library menu should have pocket button");
   is(pocketLibraryButton.disabled, false, "element appMenu-library-pocket-button is not disabled");
 
   info("clicking on pocket library button");
   let pocketPagePromise = BrowserTestUtils.waitForNewTab(gBrowser,
-    "https://example.com/browser/browser/components/pocket/test/pocket_actions_test.html/?src=ff_ext");
+    "https://example.com/browser/browser/components/pocket/test/pocket_actions_test.html/firefox_learnmore?src=ff_library");
   pocketLibraryButton.click();
   await pocketPagePromise;
 
   is(gBrowser.currentURI.spec,
-    "https://example.com/browser/browser/components/pocket/test/pocket_actions_test.html/?src=ff_ext",
+    "https://example.com/browser/browser/components/pocket/test/pocket_actions_test.html/firefox_learnmore?src=ff_library",
     "pocket button in library menu button opens correct page");
 
   BrowserTestUtils.removeTab(tab);
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -378,17 +378,17 @@
            data-l10n-id="download-always-ask-where"/>
   </radiogroup>
 </groupbox>
 
 <groupbox id="applicationsGroup" data-category="paneGeneral" hidden="true">
   <label><html:h2 data-l10n-id="applications-header"/></label>
   <description data-l10n-id="applications-description"/>
   <textbox id="filter" flex="1"
-           type="search"
+           is="search-textbox"
            data-l10n-id="applications-filter"
            aria-controls="handlersView"/>
 
   <listheader equalsize="always">
     <treecol id="typeColumn" data-l10n-id="applications-type-column" value="type"
              persist="sortDirection"
              flex="1" sortDirection="ascending"/>
     <treecol id="actionColumn" data-l10n-id="applications-action-column" value="action"
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -168,17 +168,17 @@
             <hbox align="top">
               <image class="info-icon"></image>
             </hbox>
             <hbox align="center" flex="1">
               <label class="policies-label" flex="1" data-l10n-id="policies-notice"></label>
             </hbox>
           </hbox>
           <textbox
-            type="search" id="searchInput"
+            is="search-textbox" id="searchInput"
             data-l10n-id="search-input-box"
             data-l10n-attrs="style"
             hidden="true" clickSelectsAll="true"/>
         </hbox>
         <vbox id="mainPrefPane">
 #include searchResults.xul
 #include main.xul
 #include home.xul
--- a/browser/components/preferences/siteDataSettings.xul
+++ b/browser/components/preferences/siteDataSettings.xul
@@ -26,17 +26,17 @@
 
   <script src="chrome://browser/content/preferences/siteDataSettings.js"/>
 
   <vbox flex="1" class="contentPane">
     <description id="settingsDescription" data-l10n-id="site-data-settings-description"/>
     <separator class="thin"/>
 
     <hbox id="searchBoxContainer">
-      <textbox id="searchBox" type="search" flex="1"
+      <textbox id="searchBox" is="search-textbox" flex="1"
         data-l10n-id="site-data-search-textbox"/>
     </hbox>
     <separator class="thin"/>
 
     <listheader>
       <treecol flex="4" width="50" data-l10n-id="site-data-column-host" id="hostCol"/>
       <treecol flex="1" width="50" data-l10n-id="site-data-column-cookies" id="cookiesCol"/>
       <!-- Sorted by usage so the user can quickly see which sites use the most data. -->
--- a/browser/components/preferences/sitePermissions.xul
+++ b/browser/components/preferences/sitePermissions.xul
@@ -31,17 +31,17 @@
     <key data-l10n-id="permissions-close-key" modifiers="accel" oncommand="window.close();"/>
   </keyset>
 
   <vbox class="contentPane">
     <description id="permissionsText" control="url"/>
     <separator class="thin"/>
     <hbox align="start">
       <textbox id="searchBox" flex="1" data-l10n-id="permissions-searchbox"
-               type="search" oncommand="gSitePermissionsManager.buildPermissionsList();"/>
+               is="search-textbox" oncommand="gSitePermissionsManager.buildPermissionsList();"/>
     </hbox>
     <separator class="thin"/>
     <listheader>
       <treecol id="siteCol" data-l10n-id="permissions-site-name" flex="3" width="50"
                onclick="gSitePermissionsManager.buildPermissionsList(event.target)"/>
       <treecol id="statusCol" data-l10n-id="permissions-status" flex="1" width="50"
                data-isCurrentSortCol="true"
                onclick="gSitePermissionsManager.buildPermissionsList(event.target);"/>
@@ -61,17 +61,17 @@
             icon="clear"
             oncommand="gSitePermissionsManager.onAllPermissionsDelete();"/>
   </hbox>
 
   <spacer flex="1"/>
   <checkbox id="permissionsDisableCheckbox"/>
   <description id="permissionsDisableDescription"/>
   <spacer flex="1"/>
-  <hbox id="browserNotificationsPermissionExtensionContent" 
+  <hbox id="browserNotificationsPermissionExtensionContent"
         class="extension-controlled" align="center" hidden="true">
     <description control="disableNotificationsPermissionExtension" flex="1"/>
     <button id="disableNotificationsPermissionExtension"
             class="extension-controlled-button accessory-button"
             data-l10n-id="disable-extension"/>
   </hbox>
   <hbox class="actionButtons" align="right" flex="1">
     <button oncommand="close();" icon="close" id="cancel"
--- a/browser/components/urlbar/tests/browser/browser.ini
+++ b/browser/components/urlbar/tests/browser/browser.ini
@@ -8,18 +8,20 @@ tags=quantumbar
 support-files =
   dummy_page.html
   head.js
   head-common.js
 
 [browser_action_searchengine.js]
 [browser_action_searchengine_alias.js]
 [browser_autocomplete_a11y_label.js]
-skip-if = true # Bug 1524539 - need to fix a11y. When removing this line, uncomment the next (or fix it!).
-# skip-if = (verify && !debug && (os == 'win'))
+skip-if = (verify && !debug && (os == 'win'))
+support-files =
+  searchSuggestionEngine.xml
+  searchSuggestionEngine.sjs
 [browser_autocomplete_autoselect.js]
 [browser_autocomplete_cursor.js]
 [browser_autocomplete_edit_completed.js]
 [browser_autocomplete_enter_race.js]
 [browser_autocomplete_no_title.js]
 [browser_autocomplete_readline_navigation.js]
 skip-if = os != "mac" # Mac only feature
 [browser_autocomplete_tag_star_visibility.js]
--- a/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js
@@ -4,29 +4,70 @@
 /**
  * This test ensures that we produce good labels for a11y purposes.
  */
 
 const SUGGEST_ALL_PREF = "browser.search.suggest.enabled";
 const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 
+async function getResultText(element) {
+  await initAccessibilityService();
+  await BrowserTestUtils.waitForCondition(() => accService.getAccessibleFor(element));
+  let accessible = accService.getAccessibleFor(element);
+  return accessible.name;
+}
+
+let accService;
+async function initAccessibilityService() {
+  if (accService) {
+    return;
+  }
+  accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+    Ci.nsIAccessibilityService);
+  if (Services.appinfo.accessibilityEnabled) {
+    return;
+  }
+
+  async function promiseInitOrShutdown(init = true) {
+    await new Promise(resolve => {
+      let observe = (subject, topic, data) => {
+        Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+        // "1" indicates that the accessibility service is initialized.
+        if (data === (init ? "1" : "0")) {
+          resolve();
+        }
+      };
+      Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+    });
+  }
+  await promiseInitOrShutdown(true);
+  registerCleanupFunction(async () => {
+    accService = null;
+    await promiseInitOrShutdown(false);
+  });
+}
+
 add_task(async function switchToTab() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:about");
 
   await promiseAutocompleteResultPopup("% about");
   let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
   Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
     "Should have a switch tab result");
 
-  // XXX Bug 1524539. This fails on QuantumBar because we're producing different
-  // outputs. Once we confirm accessibilty is ok with the new format, we
-  // should update and have this test running on QuantumBar.
   let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
-  is(element.label, "about:about about:about Tab", "Result a11y label should be: <title> <url> Tab");
+  is(await getResultText(element),
+     UrlbarPrefs.get("quantumbar") ?
+       // The extra spaces are here due to bug 1550644.
+       "about : about— Switch to Tab" :
+       "about:about about:about Tab",
+     UrlbarPrefs.get("quantumbar") ?
+       "Result a11y label should be: <title>— Switch to Tab" :
+       "Result a11y label should be: <title> <url> Tab");
 
   await UrlbarTestUtils.promisePopupClose(window);
   gBrowser.removeTab(tab);
 });
 
 add_task(async function searchSuggestions() {
   let engine = await SearchTestUtils.promiseNewSearchEngine(
     getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME);
@@ -47,27 +88,37 @@ add_task(async function searchSuggestion
   // by earlier tests.
   Assert.greaterOrEqual(length, 3,
     "Should get at least heuristic result + two search suggestions");
   // The first expected search is the search term itself since the heuristic
   // result will come before the search suggestions.
   let expectedSearches = [
     "foo",
     "foofoo",
-    "foobar",
+    // The extra spaces is here due to bug 1550644.
+    UrlbarPrefs.get("quantumbar") ? "foo bar " : "foobar",
   ];
   for (let i = 0; i < length; i++) {
     let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
     if (result.type === UrlbarUtils.RESULT_TYPE.SEARCH) {
       Assert.greaterOrEqual(expectedSearches.length, 0,
         "Should still have expected searches remaining");
       let suggestion = expectedSearches.shift();
-      // XXX Bug 1524539. This fails on QuantumBar because we're producing different
-      // outputs. Once we confirm accessibilty is ok with the new format, we
-      // should update and have this test running on QuantumBar.
       let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, i);
-      Assert.equal(element.label,
-        suggestion + " browser_searchSuggestionEngine searchSuggestionEngine.xml Search",
-        "Result label should be: <search term> <engine name> Search");
+      let selected = element.hasAttribute("selected");
+      if (!selected) {
+        // Simulate the result being selected so we see the expanded text.
+        element.toggleAttribute("selected", true);
+      }
+      Assert.equal(await getResultText(element),
+        UrlbarPrefs.get("quantumbar") ?
+          suggestion + "— Search with browser_searchSuggestionEngine searchSuggestionEngine.xml" :
+          suggestion + " browser_searchSuggestionEngine searchSuggestionEngine.xml Search",
+        UrlbarPrefs.get("quantumbar") ?
+          "Result label should be: <search term>— Search with <engine name>" :
+          "Result label should be: <search term> <engine name> Search");
+      if (!selected) {
+        element.toggleAttribute("selected", false);
+      }
     }
   }
   Assert.ok(expectedSearches.length == 0);
 });
--- a/browser/config/mozconfigs/linux64/noopt-debug
+++ b/browser/config/mozconfigs/linux64/noopt-debug
@@ -1,9 +1,7 @@
-MOZ_AUTOMATION_BUILD_SYMBOLS=0
-
 # Developers often build with these options for a better debugging experience.
 . "$topsrcdir/browser/config/mozconfigs/linux64/debug"
 
 # We add this last to guard against inadvertent changes in the debug config.
 # It may conflict with settings from mozconfig.override, but that seems
 # unlikely.
 ac_add_options --disable-optimize
--- a/browser/extensions/formautofill/api.js
+++ b/browser/extensions/formautofill/api.js
@@ -125,18 +125,18 @@ this.formautofill = class extends Extens
 
     // Listen for the autocomplete popup message to lazily append our stylesheet related to the popup.
     Services.mm.addMessageListener("FormAutoComplete:MaybeOpenPopup", onMaybeOpenPopup);
 
     formAutofillParent.init().catch(Cu.reportError);
     Services.mm.loadFrameScript("chrome://formautofill/content/FormAutofillFrameScript.js", true, true);
   }
 
-  onShutdown(reason) {
-    if (reason == "APP_SHUTDOWN") {
+  onShutdown(isAppShutdown) {
+    if (isAppShutdown) {
       return;
     }
 
     resProto.setSubstitution(RESOURCE_HOST, null);
 
     this.chromeHandle.destruct();
     this.chromeHandle = null;
 
--- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
+++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
@@ -218,16 +218,24 @@ class ChromeActions {
       startAt: Date.now(),
     };
   }
 
   isInPrivateBrowsing() {
     return PrivateBrowsingUtils.isContentWindowPrivate(this.domWindow);
   }
 
+  getWindowOriginAttributes() {
+    try {
+      return this.domWindow.document.nodePrincipal.originAttributes;
+    } catch (err) {
+      return {};
+    }
+  }
+
   download(data, sendResponse) {
     var self = this;
     var originalUrl = data.originalUrl;
     var blobUrl = data.blobUrl || originalUrl;
     // The data may not be downloaded so we need just retry getting the pdf with
     // the original url.
     var originalUri = NetUtil.newURI(originalUrl);
     var filename = data.filename;
@@ -577,16 +585,19 @@ class RangedChromeActions extends Chrome
     if (originalRequest.visitRequestHeaders) {
       originalRequest.visitRequestHeaders(httpHeaderVisitor);
     }
 
     var self = this;
     var xhr_onreadystatechange = function xhr_onreadystatechange() {
       if (this.readyState === 1) { // LOADING
         var netChannel = this.channel;
+        // override this XMLHttpRequest's OriginAttributes with our cached parent window's
+        // OriginAttributes, as we are currently running under the SystemPrincipal
+        this.setOriginAttributes(self.getWindowOriginAttributes());
         if ("nsIPrivateBrowsingChannel" in Ci &&
             netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
           var docIsPrivate = self.isInPrivateBrowsing();
           netChannel.setPrivate(docIsPrivate);
         }
       }
     };
     var getXhr = function getXhr() {
--- a/browser/extensions/report-site-issue/experimentalAPIs/l10n.js
+++ b/browser/extensions/report-site-issue/experimentalAPIs/l10n.js
@@ -11,18 +11,18 @@ var {Services} = ChromeUtils.import("res
 XPCOMUtils.defineLazyGetter(this, "l10nStrings", function() {
   return Services.strings.createBundle(
     "chrome://webcompat-reporter/locale/webcompat.properties");
 });
 
 let l10nManifest;
 
 this.l10n = class extends ExtensionAPI {
-  onShutdown(reason) {
-    if (reason !== "APP_SHUTDOWN" && l10nManifest) {
+  onShutdown(isAppShutdown) {
+    if (!isAppShutdown && l10nManifest) {
       Components.manager.removeBootstrappedManifestLocation(l10nManifest);
     }
   }
   getAPI(context) {
     // Until we move to Fluent (bug 1446164), we're stuck with
     // chrome.manifest for handling localization since its what the
     // build system can handle for localized repacks.
     if (context.extension.rootURI instanceof Ci.nsIJARURI) {
--- a/browser/extensions/screenshots/_locales/ach/messages.json
+++ b/browser/extensions/screenshots/_locales/ach/messages.json
@@ -12,48 +12,71 @@
     "message": "Cal Na"
   },
   "screenshotInstructions": {
     "message": "Ywar onyo dii ii potbuk me yero bute. Dii ESC me juko."
   },
   "saveScreenshotSelectedArea": {
     "message": "Gwoki"
   },
+  "uploadScreenshotSelectedArea": {
+    "message": "Keti"
+  },
   "saveScreenshotVisibleArea": {
     "message": "Gwok ma nen"
   },
   "saveScreenshotFullPage": {
     "message": "Gwok potbuk weng"
   },
   "cancelScreenshot": {
     "message": "Juki"
   },
   "downloadScreenshot": {
     "message": "Gam"
   },
   "downloadOnlyNotice": {
     "message": "Kombedi itye i kit me gam keken."
   },
+  "downloadOnlyDetailsPrivate": {
+    "message": "I dirica me Yeny i Mung."
+  },
+  "downloadOnlyDetailsNeverRemember": {
+    "message": "“Pe ipoo ikom gin mukato” tye ma kicako."
+  },
   "downloadOnlyDetailsESR": {
     "message": "Itye ka tic ki Firefox pi ESR."
   },
+  "downloadOnlyDetailsNoUploadPref": {
+    "message": "Kijuko woko keto."
+  },
   "notificationLinkCopiedTitle": {
     "message": "Ki loko kakube"
   },
   "notificationLinkCopiedDetails": {
     "message": "Ki loko kakube me cal mamegi i bao me coc. Dii $META_KEY$-V me mwono ne.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Loki"
   },
+  "notificationImageCopiedTitle": {
+    "message": "Kiloko Cal"
+  },
+  "notificationImageCopiedDetails": {
+    "message": "Ki loko cal mamegi i bao coc. Dii $META_KEY$-V me mwono ne.",
+    "placeholders": {
+      "meta_key": {
+        "content": "$1"
+      }
+    }
+  },
   "requestErrorTitle": {
     "message": "Pe tye katic."
   },
   "requestErrorDetails": {
     "message": "Timwa kica! Pe onongo wa twero gwoko cal mamegi. Tim ber item doki lacen."
   },
   "connectionErrorTitle": {
     "message": "Pe watwero kube ki cal me wang kio mamegi."
@@ -77,40 +100,31 @@
     "message": "Yer mamegi tidi tutwal"
   },
   "genericErrorTitle": {
     "message": "Woo! Firefox Screenshots opo oo."
   },
   "genericErrorDetails": {
     "message": "Pe wa ngeyo ngo ma otime kombedi. Iromo temo ne doki onyo mako cal pa potbuk mukene?"
   },
-  "tourBodyIntro": {
-    "message": "Maki, gwoki, ki nywak cal me wang kio labongo weko Firefox."
-  },
   "tourHeaderPageAction": {
     "message": "Yoo manyen me gwoko"
   },
   "tourHeaderClickAndDrag": {
     "message": "Mak ngo ma imito keken"
   },
   "tourBodyClickAndDrag": {
     "message": "Dii ka i ywar me mako cal pa but potbuk keken. Itwero bene wot iwiye me wero yer mamegi."
   },
   "tourHeaderFullPage": {
     "message": "Mak dirica onyo Potbuk weng"
   },
   "tourBodyFullPage": {
     "message": "Yer mapeca ma i tung lacuc malo me mako kabedo ma nen i dirica onyo me mako potbuk weng."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Kit ma imito"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Gwok cal mamegi ma ki ngolo ii Kakube pi nywako i yoo ma yot, onyo gamo gi i kompiuta ni. Itwero bene diyo mapeca me Cal Na me nongo cal ma i mako weng."
-  },
   "tourSkip": {
     "message": "Kal"
   },
   "tourNext": {
     "message": "Cal malubo"
   },
   "tourPrevious": {
     "message": "Cal mukato"
--- a/browser/extensions/screenshots/_locales/az/messages.json
+++ b/browser/extensions/screenshots/_locales/az/messages.json
@@ -27,16 +27,19 @@
     "message": "Tam səhifəni saxla"
   },
   "cancelScreenshot": {
     "message": "Ləğv et"
   },
   "downloadScreenshot": {
     "message": "Endir"
   },
+  "downloadScreenshotTitle": {
+    "message": "Ekran görüntüsünü endir"
+  },
   "downloadOnlyNotice": {
     "message": "Hazırda Ancaq-Endirmə rejimindəsiniz."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots bu vəziyyətlərdə avtomatik olaraq Ancaq-Endirmə rejiminə keçir:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "Məxfi Səyahət pəncərəsində."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Köçür"
   },
+  "copyScreenshotTitle": {
+    "message": "Ekran görüntüsünü mübadilə buferinə köçür"
+  },
   "notificationImageCopiedTitle": {
     "message": "Görüntü Köçürüldü"
   },
   "notificationImageCopiedDetails": {
     "message": "Görüntünüz mübadilə buferinə köçürüldü. Yapışdırmaq üçün $META_KEY$-V basın.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Narahatlıq üçün üzr istəyirik. Gələcək buraxılışlarda bu özəllik üzərində işləyirik."
   },
   "genericErrorTitle": {
     "message": "Off! Firefox Screenshots dəli olub."
   },
   "genericErrorDetails": {
     "message": "Nə baş verdiyindən əmin deyilik. Bir daha yoxlayın və ya başqa səhifənin ekran görüntüsünü alaraq işləyib işləmədiyinə əmin olun."
   },
-  "tourBodyIntro": {
-    "message": "Firefoxu tərk etmədən ekran görüntüləri alın, saxlayın və paylaşın."
+  "tourBodyIntroServerless": {
+    "message": "Firefox səyyahınızı tərk etmədən ekran görüntülərinizi çəkin, köçürün və endirin."
   },
   "tourHeaderPageAction": {
     "message": "Saxlamağın yeni yolu"
   },
   "tourBodyPageAction": {
     "message": "Ekran görüntüsü almaq istədiyinizdə ünvan sətrindəki səhifə əməliyyatları menyusunu açın."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Səhifənin hər hansı bir hissəsini almaq üçün basın və ya sürüşdürün. Seçiminizi işıqlandırmaq üçün üzərinə gedin."
   },
   "tourHeaderFullPage": {
     "message": "Pəncərəni və ya bütün səhifəni çəkin"
   },
   "tourBodyFullPage": {
     "message": "Sadəcə pəncərədə görünən hissəni və ya bütün səhifəni çəkmək üçün sağ üstdəki düymələrdən birini seçin."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "İstədiyiniz kimi"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Kəsdiyiniz hissələri rahat paylaşmaq üçün internetdə saxlayın və ya kompüterinizə endirin. Həmçinin Ekran Görüntülərim düyməsinə basaraq çəkdiyiniz bütün ekran görüntülərini görə bilərsiz."
-  },
-  "tourHeaderAccounts": {
-    "message": "Ekran Görüntülərin həmişə yanında"
-  },
-  "tourBodyAccounts": {
-    "message": "Firefox Hesabınız ilə daxil olun və istənilən yerdə bütün alətlərinizdən görüntülərinizi görün və seçdiklərinizi daimi olaraq saxlayın."
-  },
   "tourSkip": {
     "message": "Ötür"
   },
   "tourNext": {
     "message": "Növbəti Slayd"
   },
   "tourPrevious": {
     "message": "Əvvəlki Slayd"
--- a/browser/extensions/screenshots/_locales/bg/messages.json
+++ b/browser/extensions/screenshots/_locales/bg/messages.json
@@ -27,16 +27,19 @@
     "message": "Запазване на цялата страница"
   },
   "cancelScreenshot": {
     "message": "Отказ"
   },
   "downloadScreenshot": {
     "message": "Изтегляне"
   },
+  "downloadScreenshotTitle": {
+    "message": "Изтегляне на екранна снимка"
+  },
   "downloadOnlyNotice": {
     "message": "В момента сте в режим на изтегляне."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots автоматично преминава в режим на изтегляне в следните ситуации"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "в поверителен прозорец"
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Копиране"
   },
+  "copyScreenshotTitle": {
+    "message": "Копиране в системния буфер"
+  },
   "notificationImageCopiedTitle": {
     "message": "Снимката е копирана"
   },
   "notificationImageCopiedDetails": {
     "message": "Снимката е копирана в системния буфер. За да я поставите натиснете $META_KEY$-V.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Съжаляваме за неудобството. Очаквайте тази възможност в бъдещите версии."
   },
   "genericErrorTitle": {
     "message": "Леле! Нещо се обърка с Firefox Screenshots."
   },
   "genericErrorDetails": {
     "message": "Не сме сигурни какво точно се случи. Може да опитате отново, както и да снимате друга страница."
   },
-  "tourBodyIntro": {
-    "message": "Създавайте, запазвайте и споделяйте снимки на екрана без да напускате Firefox."
+  "tourBodyIntroServerless": {
+    "message": "Създавайте, копирайте и изтегляйте снимки на екрана без да напускате Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Нов начин за запазване"
   },
   "tourBodyPageAction": {
     "message": "Отворете менюто за действия със страницата, което се намира в адресната лента, когато желаете да направите снимка на екрана."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Щракнете с мишката или влачете, за да уловите части от страницата. А когато посочите елементи от страницата – те се осветяват."
   },
   "tourHeaderFullPage": {
     "message": "Улавяйте прозорци и цели страници"
   },
   "tourBodyFullPage": {
     "message": "Използвайте бутоните в горния десен ъгъл, за да уловите само видимата част или цялата страница."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Както ви харесва"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Запазвайте снимките на страници от Мрежата, за да ги споделяте по-лесно или ги изтегляйте на компютъра си. А бутонът „Моите снимки“ ще ви покаже всички направени от вас снимки."
-  },
-  "tourHeaderAccounts": {
-    "message": "Снимки на екрана за из път"
-  },
-  "tourBodyAccounts": {
-    "message": "Впишете се в Снимки на екрана с вашия Firefox Account, за да получите достъп до вашите снимки на всички ваши устройства и за да запазите вашите любими снимки завинаги."
-  },
   "tourSkip": {
     "message": "Пропускане"
   },
   "tourNext": {
     "message": "Напред"
   },
   "tourPrevious": {
     "message": "Назад"
--- a/browser/extensions/screenshots/_locales/bn_BD/messages.json
+++ b/browser/extensions/screenshots/_locales/bn_BD/messages.json
@@ -4,32 +4,32 @@
   },
   "addonAuthorsList": {
     "message": "Mozilla <screenshots-feedback@mozilla.com>"
   },
   "contextMenuLabel": {
     "message": "একটি স্ক্রিনশট নিন"
   },
   "myShotsLink": {
-    "message": "আমার সটসমূহ"
+    "message": "আমার সট"
   },
   "screenshotInstructions": {
-    "message": "ড্রাগ করে অথবা পেজে ক্লিক করে একটি অংশ নির্বাচন করুন। বাতিল করতে ESC টিপুন।"
+    "message": "ড্রাগ করে অথবা পাতায় ক্লিক করে একটি অংশ নির্বাচন করুন। বাতিল করতে ESC চাপুন।"
   },
   "saveScreenshotSelectedArea": {
     "message": "সংরক্ষণ"
   },
   "uploadScreenshotSelectedArea": {
     "message": "আপলোড"
   },
   "saveScreenshotVisibleArea": {
     "message": "যতটুকু দেখা যাচ্ছে সংরক্ষণ করুন"
   },
   "saveScreenshotFullPage": {
-    "message": "সম্পূর্ণ পেজ সংরক্ষণ করুন"
+    "message": "সম্পূর্ণ পাতা সংরক্ষণ করুন"
   },
   "cancelScreenshot": {
     "message": "বাতিল"
   },
   "downloadScreenshot": {
     "message": "ডাউনলোড"
   },
   "downloadScreenshotTitle": {
@@ -46,23 +46,23 @@
   },
   "downloadOnlyDetailsThirdParty": {
     "message": "তৃতীয়-পক্ষীয় কুকি নিষ্ক্রিয় আছে।"
   },
   "downloadOnlyDetailsNeverRemember": {
     "message": "“ইতিহাস কখনো মনে রাখবেন না” সক্রিয় হয়েছে।"
   },
   "downloadOnlyDetailsESR": {
-    "message": "অাপনি Firefox ESR ব্যবহার করছেন।"
+    "message": "আপনি Firefox ESR ব্যবহার করছেন।"
   },
   "downloadOnlyDetailsNoUploadPref": {
     "message": "আপলোড নিষ্ক্রিয় করা হয়েছে।"
   },
   "notificationLinkCopiedTitle": {
-    "message": "লিঙ্ক কপি করা হয়েছে"
+    "message": "লিঙ্ক অনুলিপি করা হয়েছে"
   },
   "notificationLinkCopiedDetails": {
     "message": "আপার সট এর লিংক ক্লিপবোর্ডে কপি করা হয়েছে। পেস্ট করতে $META_KEY$-V চাপুন।",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
@@ -80,17 +80,17 @@
     "message": "আপনার শট ক্লিপবোর্ডে অনুলিপি করা হয়েছে। প্রতিলেপন করতে $META_KEY$-V চাপুন।",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "imageCropPopupWarning": {
-    "message": "সংরক্ষিত ইমেজ $PIXELS$পিক্সেল উচ্চতায় ক্রপ করা হবে।",
+    "message": "সংরক্ষিত চিত্র $PIXELS$পিক্সেল উচ্চতায় কাটা হবে।",
     "placeholders": {
       "pixels": {
         "content": "$1"
       }
     }
   },
   "requestErrorTitle": {
     "message": "বিকল।"
@@ -103,23 +103,23 @@
   },
   "connectionErrorDetails": {
     "message": "অনুগ্রহ করে আপনার ইন্টারনেট সংযোগ পরীক্ষা করুন। আর যদি আপনার ইন্টারনেট সংযোগ ঠিক থাকে, তাহলে Firefox স্ক্রিনশট সেবাটিতে সাময়িক সমস্যা দেখা দিয়েছে।"
   },
   "loginErrorDetails": {
     "message": "আমরা আপনার শট সংরক্ষণ করতে পারিনি কারণ সেখানে Firefox স্ক্রিণশট সেবার সমস্যা আছে। অনুগ্রহ করে আবার চেস্টা করুন।"
   },
   "unshootablePageErrorTitle": {
-    "message": "আমার এই পেজের স্ক্রিনশট নিতে পারব না।"
+    "message": "আমরা এই পাতার স্ক্রিনশট নিতে পারব না।"
   },
   "unshootablePageErrorDetails": {
-    "message": "এটা কোন প্রমিত ওয়েব পেজ না, তাই আপনি এটার স্ক্রিনশট তুলতে পারবেন না।"
+    "message": "এটা কোন আদর্শ ওয়েব পাতা না, তাই আপনি এটার স্ক্রিনশট নিতে পারবেন না।"
   },
   "selfScreenshotErrorTitle": {
-    "message": "আপনি Firefox স্ক্রিনশটের পেজের শট নিতে পারেন না!"
+    "message": "আপনি Firefox Screenshots পাতার শট নিতে পারবেন না!"
   },
   "emptySelectionErrorTitle": {
     "message": "আপনি অল্প স্থান নির্বাচন করেছেন"
   },
   "privateWindowErrorTitle": {
     "message": "ব্যক্তিগত ব্রাউজিং মোডে স্ক্রিনশট নেওয়া নিস্ক্রিয় করা হয়েছে"
   },
   "privateWindowErrorDetails": {
@@ -145,17 +145,17 @@
   },
   "tourBodyClickAndDrag": {
     "message": "পাতার কিয়দংশ ক্যাপচার করতে ক্লিক করে ড্রাগ করুন। অতঃপর আপনি মাউস হোভার করে আপনার নির্বাচিত অংশ হাইলাইট করতে পারবেন।"
   },
   "tourHeaderFullPage": {
     "message": "উইন্ডো অথবা সম্পূর্ণ পাতা ক্যাপচার করুন"
   },
   "tourBodyFullPage": {
-    "message": "ইউন্ডোতে দৃশ্যমান অংশ অথবা সম্পূর্ণ পাতা ক্যাপচার করতে উপরে ডানদিকের বাটনগুলো থেকে নির্বাচন করুন।"
+    "message": "উইন্ডোতে দৃশ্যমান অংশ অথবা সম্পূর্ণ পাতা ক্যাপচার করতে উপরে ডানদিকের বোতাম থেকে নির্বাচন করুন।"
   },
   "tourSkip": {
     "message": "এড়িয়ে যান"
   },
   "tourNext": {
     "message": "পরবর্তী স্লাইড"
   },
   "tourPrevious": {
--- a/browser/extensions/screenshots/_locales/cak/messages.json
+++ b/browser/extensions/screenshots/_locales/cak/messages.json
@@ -12,16 +12,19 @@
     "message": "Taq Nuwachib'al"
   },
   "screenshotInstructions": {
     "message": "Taqirirej o tapitz'a' ri ruxaq richin nacha' ri k'ojlem. Tapitz'a' ESC richin niq'at."
   },
   "saveScreenshotSelectedArea": {
     "message": "Tiyak"
   },
+  "uploadScreenshotSelectedArea": {
+    "message": "Tijotob'äx"
+  },
   "saveScreenshotVisibleArea": {
     "message": "Tiyak wachel"
   },
   "saveScreenshotFullPage": {
     "message": "Tiyak chijun ruxaq"
   },
   "cancelScreenshot": {
     "message": "Tiq'at"
@@ -117,19 +120,16 @@
     "message": "Takuyu' chi qe ruma ri k'ayewal. Tajin niqasamajij re rub'anikil re' richin ri ch'aqa' chik taq ruwäch."
   },
   "genericErrorTitle": {
     "message": "¡Itz! Itzel xe'el ri Firefox Chapoj Wachib'äl."
   },
   "genericErrorDetails": {
     "message": "Man öj jikïl chi rij ri xk'ulwachitäj. ¿La nawajo' natojtob'ej chik o nachäp ruwachib'al jun chik ruxaq?"
   },
-  "tourBodyIntro": {
-    "message": "Ke'achapa', ke'ayaka', chuqa' ke'akomonij chapoj taq wachib'äl rik'in man yatel ta el pa Firefox."
-  },
   "tourHeaderPageAction": {
     "message": "Jun k'ak'a' rub'anikil richin niyak"
   },
   "tourBodyPageAction": {
     "message": "Tarik'a' ri ruk'utsamaj kisamaj taq ruxaq pa kik'ajtz'ik ochochib'äl xab'achike ramaj toq nawajo' nawelesaj jun chapoj wachib'äl."
   },
   "tourHeaderClickAndDrag": {
     "message": "Tachapa' ri Nawajo'"
@@ -138,22 +138,16 @@
     "message": "Tapitz'a' chuqa' taqirirej richin nacha' xa jun peraj ruxaq. Chuqa' yatikïr yaq'axaj richin nipe retal ri acha'oj."
   },
   "tourHeaderFullPage": {
     "message": "Chapoj Tzuwäch o Tz'aqät taq Ruxaq"
   },
   "tourBodyFullPage": {
     "message": "Ke'acha' ri ikim ajkiq'a' taq pitz'b'äl richin nachäp ri tz'etel ruk'ojlem tzuwäch o richin nachäp jun tz'aqät ruxaq."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Achi'el Niqa Chawäch"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Ke'ayaka' ri qupin taq awachib'al pa ajk'amaya'l richin man k'ayew ta ye'akomonij o ye'aqasaj pan akematz'ib'. Chuqa' yatikïr napïtz ri Taq Nuwachib'al richin ye'awïl konojel ri taq wachib'al e'elesan."
-  },
   "tourSkip": {
     "message": "SKIP"
   },
   "tourNext": {
     "message": "Jun chik Q'axewäch"
   },
   "tourPrevious": {
     "message": "Jun kan Q'axewäch"
deleted file mode 100644
--- a/browser/extensions/screenshots/_locales/en_GB/messages.json
+++ /dev/null
@@ -1,193 +0,0 @@
-{
-  "addonDescription": {
-    "message": "Take clips and screenshots from the Web and save them temporarily or permanently."
-  },
-  "addonAuthorsList": {
-    "message": "Mozilla <screenshots-feedback@mozilla.com>"
-  },
-  "contextMenuLabel": {
-    "message": "Take a Screenshot"
-  },
-  "myShotsLink": {
-    "message": "My Shots"
-  },
-  "screenshotInstructions": {
-    "message": "Drag or click on the page to select a region. Press ESC to cancel."
-  },
-  "saveScreenshotSelectedArea": {
-    "message": "Save"
-  },
-  "uploadScreenshotSelectedArea": {
-    "message": "Upload"
-  },
-  "saveScreenshotVisibleArea": {
-    "message": "Save visible"
-  },
-  "saveScreenshotFullPage": {
-    "message": "Save full page"
-  },
-  "cancelScreenshot": {
-    "message": "Cancel"
-  },
-  "downloadScreenshot": {
-    "message": "Download"
-  },
-  "downloadOnlyNotice": {
-    "message": "You are currently in Download-Only mode."
-  },
-  "downloadOnlyDetails": {
-    "message": "Firefox Screenshots automatically changes to Download-Only mode in these situations:"
-  },
-  "downloadOnlyDetailsPrivate": {
-    "message": "In a Private Browsing window."
-  },
-  "downloadOnlyDetailsThirdParty": {
-    "message": "Third-party cookies are disabled."
-  },
-  "downloadOnlyDetailsNeverRemember": {
-    "message": "“Never remember history” is enabled."
-  },
-  "downloadOnlyDetailsESR": {
-    "message": "You are using Firefox ESR."
-  },
-  "downloadOnlyDetailsNoUploadPref": {
-    "message": "Uploads have been disabled."
-  },
-  "notificationLinkCopiedTitle": {
-    "message": "Link Copied"
-  },
-  "notificationLinkCopiedDetails": {
-    "message": "The link to your shot has been copied to the clipboard. Press $META_KEY$-V to paste.",
-    "placeholders": {
-      "meta_key": {
-        "content": "$1"
-      }
-    }
-  },
-  "copyScreenshot": {
-    "message": "Copy"
-  },
-  "notificationImageCopiedTitle": {
-    "message": "Shot Copied"
-  },
-  "notificationImageCopiedDetails": {
-    "message": "Your shot has been copied to the clipboard. Press $META_KEY$-V to paste.",
-    "placeholders": {
-      "meta_key": {
-        "content": "$1"
-      }
-    }
-  },
-  "imageCropPopupWarning": {
-    "message": "Saved image will be cropped to $PIXELS$px in height.",
-    "placeholders": {
-      "pixels": {
-        "content": "$1"
-      }
-    }
-  },
-  "requestErrorTitle": {
-    "message": "Out of order."
-  },
-  "requestErrorDetails": {
-    "message": "Sorry! We couldn’t save your shot. Please try again later."
-  },
-  "connectionErrorTitle": {
-    "message": "We can’t connect to your screenshots."
-  },
-  "connectionErrorDetails": {
-    "message": "Please check your Internet connection. If you are able to connect to the Internet, there may be a temporary problem with the Firefox Screenshots service."
-  },
-  "loginErrorDetails": {
-    "message": "We couldn’t save your shot because there is a problem with the Firefox Screenshots service. Please try again later."
-  },
-  "unshootablePageErrorTitle": {
-    "message": "We can’t screenshot this page."
-  },
-  "unshootablePageErrorDetails": {
-    "message": "This isn’t a standard Web page, so you can’t take a screenshot of it."
-  },
-  "selfScreenshotErrorTitle": {
-    "message": "You can’t take a shot of a Firefox Screenshots page!"
-  },
-  "emptySelectionErrorTitle": {
-    "message": "Your selection is too small"
-  },
-  "privateWindowErrorTitle": {
-    "message": "Screenshots is disabled in Private Browsing Mode"
-  },
-  "privateWindowErrorDetails": {
-    "message": "Sorry for the inconvenience. We are working on this feature for future releases."
-  },
-  "genericErrorTitle": {
-    "message": "Whoa! Firefox Screenshots went haywire."
-  },
-  "genericErrorDetails": {
-    "message": "We’re not sure what just happened. Care to try again or take a shot of a different page?"
-  },
-  "tourBodyIntro": {
-    "message": "Take, save, and share screenshots without leaving Firefox."
-  },
-  "tourHeaderPageAction": {
-    "message": "A new way to save"
-  },
-  "tourBodyPageAction": {
-    "message": "Expand the page actions menu in the address bar any time you want to take a screenshot."
-  },
-  "tourHeaderClickAndDrag": {
-    "message": "Capture Just What You Want"
-  },
-  "tourBodyClickAndDrag": {
-    "message": "Click and drag to capture just a portion of a page. You can also hover to highlight your selection."
-  },
-  "tourHeaderFullPage": {
-    "message": "Capture Windows or Entire Pages"
-  },
-  "tourBodyFullPage": {
-    "message": "Select the buttons in the upper right to capture the visible area in the window or to capture an entire page."
-  },
-  "tourHeaderDownloadUpload": {
-    "message": "As You Like It"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Save your cropped shots to the web for easier sharing, or download them to your computer. You also can click on the My Shots button to find all the shots you’ve taken."
-  },
-  "tourHeaderAccounts": {
-    "message": "Screenshots to Go"
-  },
-  "tourBodyAccounts": {
-    "message": "Sign in with your Firefox Account to access your shots on all of your devices and save your favourite shots forever."
-  },
-  "tourSkip": {
-    "message": "SKIP"
-  },
-  "tourNext": {
-    "message": "Next Slide"
-  },
-  "tourPrevious": {
-    "message": "Previous Slide"
-  },
-  "tourDone": {
-    "message": "Done"
-  },
-  "termsAndPrivacyNotice2": {
-    "message": "By using Firefox Screenshots, you agree to our $TERMSANDPRIVACYNOTICETERMSLINK$ and $TERMSANDPRIVACYNOTICEPRIVACYLINK$.",
-    "placeholders": {
-      "termsandprivacynoticetermslink": {
-        "content": "$1"
-      },
-      "termsandprivacynoticeprivacylink": {
-        "content": "$2"
-      }
-    }
-  },
-  "termsAndPrivacyNoticeTermsLink": {
-    "message": "Terms"
-  },
-  "termsAndPrivacyNoticyPrivacyLink": {
-    "message": "Privacy Notice"
-  },
-  "libraryLabel": {
-    "message": "Screenshots"
-  }
-}
\ No newline at end of file
--- a/browser/extensions/screenshots/_locales/es_CL/messages.json
+++ b/browser/extensions/screenshots/_locales/es_CL/messages.json
@@ -65,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Copiar"
   },
+  "copyScreenshotTitle": {
+    "message": "Copiar captura de pantalla al portapapeles"
+  },
   "notificationImageCopiedTitle": {
     "message": "Captura copiada"
   },
   "notificationImageCopiedDetails": {
     "message": "Tu captura ha sido copiada al portapapeles. Presiona $META_KEY$-V para pegarla.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -123,16 +126,19 @@
     "message": "Disculpa las molestias. Estamos trabajando en esta función para una futura versión."
   },
   "genericErrorTitle": {
     "message": "¡Guau! Firefox Screenshots se copetió."
   },
   "genericErrorDetails": {
     "message": "No estamos seguros de lo que sucedió. ¿Te importaría volver a intentarlo o tomar una captura de una página diferente?"
   },
+  "tourBodyIntroServerless": {
+    "message": "Toma, copia y descarga capturas de pantalla sin salir de Firefox."
+  },
   "tourHeaderPageAction": {
     "message": "Una nueva forma de guardar"
   },
   "tourBodyPageAction": {
     "message": "Expande el menú de acciones de página en la barra de direcciones en cualquier momento en que quieras tomar una captura."
   },
   "tourHeaderClickAndDrag": {
     "message": "Captura lo que necesitas"
--- a/browser/extensions/screenshots/_locales/es_MX/messages.json
+++ b/browser/extensions/screenshots/_locales/es_MX/messages.json
@@ -27,16 +27,19 @@
     "message": "Guardar página completa"
   },
   "cancelScreenshot": {
     "message": "Cancelar"
   },
   "downloadScreenshot": {
     "message": "Descarga"
   },
+  "downloadScreenshotTitle": {
+    "message": "Descargar captura de pantalla"
+  },
   "downloadOnlyNotice": {
     "message": "Estás en modo de Solo-Descargas."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots automáticamente cambia al modo Solo-Descarga en estas situaciones:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "En una ventana de navegación privada."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Copiar"
   },
+  "copyScreenshotTitle": {
+    "message": "Copiar la captura de pantalla al portapapeles"
+  },
   "notificationImageCopiedTitle": {
     "message": "Captura copiada"
   },
   "notificationImageCopiedDetails": {
     "message": "Tu captura ha sido copiada al portapapeles. Presiona $META_KEY$-V para pegar.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Disculpen las molestias. Estamos trabajando en esta característica para las versiones futuras."
   },
   "genericErrorTitle": {
     "message": "¡Oye! Las capturas de pantalla de Firefox salieron mal."
   },
   "genericErrorDetails": {
     "message": "No estamos seguros qué pasó. ¿Te importaría intentarlo de nuevo o tomar una captura de una página diferente?"
   },
-  "tourBodyIntro": {
-    "message": "Toma, guarda y comparte capturas de pantalla sin dejar Firefox."
+  "tourBodyIntroServerless": {
+    "message": "Toma, copia y descarga capturas de pantalla sin salir de Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Una nueva forma de guardar"
   },
   "tourBodyPageAction": {
     "message": "Expande el menú de acciones de la página en la barra de direcciones en cualquier momento que quieras tomar una captura de pantalla."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Haz clic y arrastra para capturas sólo una parte de la página. También puedes desplazarte para resaltar tu selección."
   },
   "tourHeaderFullPage": {
     "message": "Captura ventanas o páginas enteras"
   },
   "tourBodyFullPage": {
     "message": "Selecciona los botones en la parte superior derecha para capturar el área visible en la ventana o para capturar una página completa."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Como te gusta"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Guarda tus capturas recortadas en la Web para compartirlas más fácilmente o descárgalas en tu computadora. También puedes hacer clic en el botón Mis Capturas para encontrar todas las fotos que has tomado."
-  },
-  "tourHeaderAccounts": {
-    "message": "Capturas en todos los dispositivos"
-  },
-  "tourBodyAccounts": {
-    "message": "Inicia sesión con tu cuenta de Firefox para acceder a tus capturas en todos tus dispositivos y guarda tus capturas favoritas para siempre."
-  },
   "tourSkip": {
     "message": "Ignorar"
   },
   "tourNext": {
     "message": "Siguiente diapositiva"
   },
   "tourPrevious": {
     "message": "Diapositiva anterior"
--- a/browser/extensions/screenshots/_locales/fa/messages.json
+++ b/browser/extensions/screenshots/_locales/fa/messages.json
@@ -27,16 +27,19 @@
     "message": "ذخیره صفحه کامل"
   },
   "cancelScreenshot": {
     "message": "لغو"
   },
   "downloadScreenshot": {
     "message": "دریافت"
   },
+  "downloadScreenshotTitle": {
+    "message": "دریافت تصاویرگرفته شده از صفحه"
+  },
   "downloadOnlyNotice": {
     "message": "شما در حال حاضر در حالت فقط-دریافت هستید."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots در خصوص چنین موقعیتی به صورت خودکارحالت را به تنها دریافت تبدیل می‌کند:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "در پنجره‌های مرور ناشناس."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "رونوشت"
   },
+  "copyScreenshotTitle": {
+    "message": "برداشت تصاویرگرفته شده از صفحه به کلیپ بورد"
+  },
   "notificationImageCopiedTitle": {
     "message": "رونوشت تصویر تهیه شد"
   },
   "notificationImageCopiedDetails": {
     "message": "عکس شما در کلیپ‌بورد رونوشت شد. $META_KEY$-V را برای جای‌گذاری فشار دهید.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "به خاطر مزاحمت متاسفیم. ما در حال کار روی این ویژگی برای انتشار‌های آینده هستیم."
   },
   "genericErrorTitle": {
     "message": "اوه! سرویس تصاویر صفحه فایرفاکس قاطی کرده."
   },
   "genericErrorDetails": {
     "message": "مطمئن نیستیم چه اتفاقی افتاده است. می‌خواهید دوباره امتحان کنید یا از یک صفحهٔ دیگر عکس بگیرید؟"
   },
-  "tourBodyIntro": {
-    "message": "بدون خارج شدن از فایرفاکس، عکس بگیرید، ذخیره کنید و به اشتراک بگذارید."
+  "tourBodyIntroServerless": {
+    "message": "گرفتن،‌ برداشت و دریافت تصاویر گرفته شده از صفحه بدون ترک کردن فایرفاکس."
   },
   "tourHeaderPageAction": {
     "message": "روش جدیدی برای ذخیره کردن"
   },
   "tourBodyPageAction": {
     "message": "بازکردن صفحه اقدامات فهرست در آدرس بار هر زمانی که شما تمایل داشته باشید از صفحه عکس بگیرید."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "کلیک کنید و بکشید تا فقط از قسمتی از صفحه عکس بگیرید. می‌توانید برای برجسته کردن روی ناحیه انتخاب شده حرکت کنید."
   },
   "tourHeaderFullPage": {
     "message": "ضبط پنجره یا کل صفحه‌ها"
   },
   "tourBodyFullPage": {
     "message": "برای گرفتن عکس از ناحیه قابل مشاهده در پنجره یا تمام صفحه از دکمه‌های بالا سمت راست استفاده کنید."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "همانطور که می‌پسندید"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "عکس‌های بریده شده خود را برای به اشتراک‌گذاری راحت‌تر روی وب ذخیره کنید، یا آن‌ها را روی رایانه خود دریافت کنید. همچنین برای دیدن همهٔ عکس‌هایی که گرفتید می‌توانید روی دکمه «عکس‌های من» کلیک کنید."
-  },
-  "tourHeaderAccounts": {
-    "message": "عکس از صفحه بلافاصله آماده برای استفاده"
-  },
-  "tourBodyAccounts": {
-    "message": "وارد حساب فایرفاکس خودتون بشید و به تمام تصاویر که توسط دستگاه‌های خودتون گرفتید دسترسی داشته باشید و تصویر مورد علاقه خودتون را برای همیشه ذخیره کنید."
-  },
   "tourSkip": {
     "message": "رد کردن"
   },
   "tourNext": {
     "message": "اسلاید بعدی"
   },
   "tourPrevious": {
     "message": "اسلاید قبلی"
--- a/browser/extensions/screenshots/_locales/fi/messages.json
+++ b/browser/extensions/screenshots/_locales/fi/messages.json
@@ -126,16 +126,19 @@
     "message": "Anteeksi häiriö. Tämä ominaisuus on vielä työn alla."
   },
   "genericErrorTitle": {
     "message": "Oho! Firefox Screenshots meni päin prinkkalaa."
   },
   "genericErrorDetails": {
     "message": "Emme oikein tiedä, mitä tapahtui. Haluatko yrittää uudestaan tai ottaa kuvan eri sivusta?"
   },
+  "tourBodyIntroServerless": {
+    "message": "Ota, kopioi ja lataa kuvakaappauksia poistumatta Firefoxista."
+  },
   "tourHeaderPageAction": {
     "message": "Uusi tapa tallentaa"
   },
   "tourBodyPageAction": {
     "message": "Avaa osoitepalkissa oleva Sivun toiminnot -valikko milloin vain, kun haluat ottaa kuvakaappauksen."
   },
   "tourHeaderClickAndDrag": {
     "message": "Kaappaa mitä haluat"
--- a/browser/extensions/screenshots/_locales/fr/messages.json
+++ b/browser/extensions/screenshots/_locales/fr/messages.json
@@ -27,16 +27,19 @@
     "message": "Capturer la page complète"
   },
   "cancelScreenshot": {
     "message": "Annuler"
   },
   "downloadScreenshot": {
     "message": "Télécharger"
   },
+  "downloadScreenshotTitle": {
+    "message": "Télécharger la capture d’écran"
+  },
   "downloadOnlyNotice": {
     "message": "Vous êtes actuellement dans un mode ne permettant que le téléchargement."
   },
   "downloadOnlyDetails": {
     "message": "Dans les situations suivantes, Firefox Screenshots permet uniquement les téléchargements :"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "lorsque vous naviguez en navigation privée."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Copier"
   },
+  "copyScreenshotTitle": {
+    "message": "Copier la capture d’écran dans le presse-papiers"
+  },
   "notificationImageCopiedTitle": {
     "message": "Capture copiée"
   },
   "notificationImageCopiedDetails": {
     "message": "Votre capture a été copiée dans le presse-papiers. Appuyez sur $META_KEY$-V pour la coller.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Désolé pour la gêne occasionnée. Nous travaillons sur cette fonctionnalité pour de prochaines versions."
   },
   "genericErrorTitle": {
     "message": "Firefox Screenshots semble avoir un problème."
   },
   "genericErrorDetails": {
     "message": "Un problème non identifié est survenu. Vous pouvez réessayer ou effectuer une capture d’écran d’une autre page."
   },
-  "tourBodyIntro": {
-    "message": "Effectuez des captures d’écran, enregistrez et partagez-les sans quitter Firefox."
+  "tourBodyIntroServerless": {
+    "message": "Prenez, copiez et téléchargez des captures d’écran sans quitter Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Une nouvelle façon d’enregistrer ses captures"
   },
   "tourBodyPageAction": {
     "message": "Dès que vous voulez effectuer une capture d’écran, il vous suffit d’ouvrir le menu d’actions de la page, depuis la barre d’adresse."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Cliquez et glissez pour capturer seulement une partie de la page. Vous pouvez aussi survoler une zone avec votre curseur pour surligner votre sélection."
   },
   "tourHeaderFullPage": {
     "message": "Effectuez des captures d’écran de fenêtres ou de pages entières"
   },
   "tourBodyFullPage": {
     "message": "Utilisez les boutons en haut à droite pour capturer au choix la zone visible dans la fenêtre ou la page entière."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "À votre guise"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Sauvegardez en ligne vos captures recadrées pour les partager plus facilement, ou téléchargez-les sur votre ordinateur. Vous pouvez aussi cliquer sur « Mes captures d’écran » pour retrouver toutes vos captures."
-  },
-  "tourHeaderAccounts": {
-    "message": "Captures à emporter"
-  },
-  "tourBodyAccounts": {
-    "message": "Connectez-vous avec votre compte Firefox pour accéder à vos captures sur tous vos appareils et enregistrer définitivement vos préférées."
-  },
   "tourSkip": {
     "message": "IGNORER"
   },
   "tourNext": {
     "message": "Écran suivant"
   },
   "tourPrevious": {
     "message": "Écran précédent"
--- a/browser/extensions/screenshots/_locales/gd/messages.json
+++ b/browser/extensions/screenshots/_locales/gd/messages.json
@@ -27,16 +27,19 @@
     "message": "Sàbhail an duilleag shlàn"
   },
   "cancelScreenshot": {
     "message": "Sguir dheth"
   },
   "downloadScreenshot": {
     "message": "Luchdaich a-nuas"
   },
+  "downloadScreenshotTitle": {
+    "message": "Luchdaich a-nuas an glacadh-sgrìn"
+  },
   "downloadOnlyNotice": {
     "message": "Tha thu sa mhodh luchdaidh a-nuas a-mhàin."
   },
   "downloadOnlyDetails": {
     "message": "Bidh gleus glacaidhean-sgrìn Firefox sa mhodh luchdaidh a-nuas gu fèin-obrachail sna suidheachaidhean a leanas:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "Ann an uinneag brabhsaidh phrìobhaidich."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Dèan lethbhreac"
   },
+  "copyScreenshotTitle": {
+    "message": "Cuir lethbhreac dhen ghlacadh-sgrìn air an stòr-bhòrd"
+  },
   "notificationImageCopiedTitle": {
     "message": "Chaidh lethbhreac a dhèanamh dhen ghlacadh"
   },
   "notificationImageCopiedDetails": {
     "message": "Chaidh lethbhreac dhen ghlacadh agad a chur air an stòr-bhòrd. Brùth $META_KEY$-V airson a chur ann.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Tha sinn duilich mu dhèidhinn. Tha sinn ag obair air agus an dòchas gum bi e ri làimh a dh’aithghearr."
   },
   "genericErrorTitle": {
     "message": "Ìoc! Sin glacaidhean-sgrìn Firefox air feadh na fìdhle."
   },
   "genericErrorDetails": {
     "message": "Chan eil sinn cinnteach dè thachair. A bheil thu airson feuchainn ris a-rithist no glacadh a thogail de dhuilleag eile?"
   },
-  "tourBodyIntro": {
-    "message": "Tog, sàbhail is co-roinn glacadh-sgrìn gun Firefix fhàgail."
+  "tourBodyIntroServerless": {
+    "message": "Tog glacaidhean-sgrìn, dèan lethbhreac dhiubh is luchdaich a-nuas iad gun Firefox fhàgail."
   },
   "tourHeaderPageAction": {
     "message": "Dòigh ùr airson sàbhaladh"
   },
   "tourBodyPageAction": {
     "message": "Leudaich clàr-taice gnìomhan na duilleige ann am bàr an t-seòlaidh uair sam bith a tha thu airson glacadh-sgrìn a thogail."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Dèan briogadh is slaodadh airson earrann de dhuilleag a ghlacadh. ’S urrainn dhut fantainn os cionn rud cuideachd airson na thagh thu a shoillseachadh."
   },
   "tourHeaderFullPage": {
     "message": "Glac uinneagan no duilleagan slàna"
   },
   "tourBodyFullPage": {
     "message": "Tagh na putanan air an taobh deas gu h-àrd airson na tha ri fhaicinn san uinneag a ghlacadh no airson duilleag shlàn a ghlacadh."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Do thoil fhèin"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Sàbhail na glacaidhean bearrte air an lìon ach am bi e furasta an co-roinneadh no luchdaich a-nuas iad dhan choimpiutair agad. ’S urrainn dhut briogadh air a’ phutan “Na glacaidhean agam” cuideachd is chì thu gach glacadh a thog thu."
-  },
-  "tourHeaderAccounts": {
-    "message": "Thoir leat glacadh"
-  },
-  "tourBodyAccounts": {
-    "message": "Clàraich a-steach leis a’ chunntas Firefox agad a dh’fhaighinn greim air na glacaidhean uile agad air feadh nan uidheaman agad is sàbhail an fheadhainn chudromach gu buan."
-  },
   "tourSkip": {
     "message": "LEUM SEACHAD"
   },
   "tourNext": {
     "message": "An ath-shleamhnag"
   },
   "tourPrevious": {
     "message": "An t-sleamhnag roimhe"
--- a/browser/extensions/screenshots/_locales/gu_IN/messages.json
+++ b/browser/extensions/screenshots/_locales/gu_IN/messages.json
@@ -27,16 +27,19 @@
     "message": "સંપૂર્ણ પૃષ્ઠ સાચવો"
   },
   "cancelScreenshot": {
     "message": "રદ"
   },
   "downloadScreenshot": {
     "message": "ડાઉનલોડ"
   },
+  "downloadScreenshotTitle": {
+    "message": "સ્ક્રીનશૉટ ડાઉનલોડ કરો"
+  },
   "downloadOnlyNotice": {
     "message": "તમે હાલમા ફક્ત ડાઉનલોડ-કરો પ્રકારમાં છો."
   },
   "downloadOnlyDetails": {
     "message": "આ પરિસ્થિતિઓ માં Firefox સ્ક્રિનશોટસ આપમેળે જ ફક્ત-ડાઉનલોડ પ્રકારમાં જતું રહેશે:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "ખાનગી બ્રાઉઝિંગ વિન્ડો માં."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "નકલ કરો"
   },
+  "copyScreenshotTitle": {
+    "message": "ક્લિપબોર્ડ પર સ્ક્રીનશોટ કૉપિ કરો"
+  },
   "notificationImageCopiedTitle": {
     "message": "શોટ નકલ કર્યો"
   },
   "notificationImageCopiedDetails": {
     "message": "તમારા શોટ ક્લિપબોર્ડ પર નકલ કરવામાં આવ્યાં છે. પેસ્ટ કરવા માટે $META_KEY$-V દબાવો.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "અસુવીધી બદલ માફી. અમે ભવિષ્યના પ્રકાશનો માટે આ સુવિધા પર કામ કરી રહ્યા છીએ."
   },
   "genericErrorTitle": {
     "message": "થોભો! Firefox સ્ક્રીનશોટ્સ અવ્યવસ્થિત થઈ ગયા."
   },
   "genericErrorDetails": {
     "message": "અમે ખાતરી નથીકે શું માત્ર થયું છે . ફરી પ્રયાસ કરો અથવા એક અલગ પૃષ્ઠ એક શોટ લેવા માટે કાળજી કરો?"
   },
-  "tourBodyIntro": {
-    "message": "લેવા, સાચવેલા, અને વહેંચાયેલ સ્ક્રીનશૉટ્સ Firefox છોડ્યાં વિના."
+  "tourBodyIntroServerless": {
+    "message": "Firefox છોડ્યાં વિના સ્ક્રીનશોટ લો, કૉપિ કરો અને ડાઉનલોડ કરો."
   },
   "tourHeaderPageAction": {
     "message": "સાચવવાનો એક નવો રસ્તો"
   },
   "tourBodyPageAction": {
     "message": "જ્યારે પણ સ્ક્રીનશૉટ લેવા માંગો ત્યારે સરનામાં બારમાં પૃષ્ઠ ક્રિયાઓ મેનૂને વિસ્તૃત કરો."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "પાનાંના માત્ર એક ભાગ મેળવવા માટે ક્લિક કરો અને ખેંચો. તમે પણ તમારી પસંદગી પ્રકાશિત કરવા માટે હૉવર કરી શકો છો."
   },
   "tourHeaderFullPage": {
     "message": "વિન્ડોઝ અથવા સમગ્ર પાના કેદ કરો"
   },
   "tourBodyFullPage": {
     "message": "ઉપર જમણા બટનો પસંદ કરો વિન્ડોમાં દૃશ્યમાન વિસ્તાર મેળવવા માટે અથવા આખુ પાનું કેપ્ચર કરવા માટે."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "તમને જે ગમે"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "સરળ શેરિંગ માટે વેબ પર તમારા કપાઈ શોટ સાચવો, અથવા તેમને તમારા કમ્પ્યુટર પર ડાઉનલોડ કરો. તમે બધા શોટ મેળવવા માટે મારું શોટ્સ બટન પર ક્લિક કરી પણ શકો છો બધા શોટ તમે લીધેલા શોધવા માટે."
-  },
-  "tourHeaderAccounts": {
-    "message": "સ્ક્રીનશોટસ ઉપયોગ માટે તૈયાર છે"
-  },
-  "tourBodyAccounts": {
-    "message": "તમારા બધા ઉપકરણો પર તમારા શોટ્સને ઍક્સેસ કરવા માટે અને તમારા મનપસંદ શોટ્સ કાયમ માટે સાચવવા માટે તમારા Firefox એકાઉન્ટથી સાઇન ઇન કરો."
-  },
   "tourSkip": {
     "message": "છોડવા"
   },
   "tourNext": {
     "message": "આગલી સ્લાઇડ"
   },
   "tourPrevious": {
     "message": "પહેલાની સ્લાઇડ"
--- a/browser/extensions/screenshots/_locales/ia/messages.json
+++ b/browser/extensions/screenshots/_locales/ia/messages.json
@@ -37,17 +37,17 @@
   },
   "downloadOnlyNotice": {
     "message": "Tu es actualmente in modo solo-discargamento."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots automaticamente se converte al modo de solo discargamento in le situationes sequente:"
   },
   "downloadOnlyDetailsPrivate": {
-    "message": "In un fenestra de Navigation Private."
+    "message": "In un Fenestra de navigation private."
   },
   "downloadOnlyDetailsThirdParty": {
     "message": "Cookies de tertie parte disactivate."
   },
   "downloadOnlyDetailsNeverRemember": {
     "message": "“Oblidar le chronologia” activate."
   },
   "downloadOnlyDetailsESR": {
@@ -115,20 +115,20 @@
   },
   "selfScreenshotErrorTitle": {
     "message": "Tu non pote prender un instantaneo de un pagina de Firefox Screenshots!"
   },
   "emptySelectionErrorTitle": {
     "message": "Tu selection es troppo micre"
   },
   "privateWindowErrorTitle": {
-    "message": "Le instantaneos es disactivate durante le navigation private"
+    "message": "Le instantaneos es disactivate durante le Navigation private"
   },
   "privateWindowErrorDetails": {
-    "message": "Pardono pro le incommoditate. Nos labora sur iste functionalitate pro futur publicationes."
+    "message": "Pardono pro le incommoditate. Nos labora sur iste functionalitate pro futur editiones."
   },
   "genericErrorTitle": {
     "message": "Problemas de Firefox Screenshots!"
   },
   "genericErrorDetails": {
     "message": "Nos non sape lo que occurreva. Reprobar o capturar un instantaneo de un altere pagina?"
   },
   "tourBodyIntroServerless": {
--- a/browser/extensions/screenshots/_locales/id/messages.json
+++ b/browser/extensions/screenshots/_locales/id/messages.json
@@ -27,16 +27,19 @@
     "message": "Simpan laman sepenuhnya"
   },
   "cancelScreenshot": {
     "message": "Batal"
   },
   "downloadScreenshot": {
     "message": "Unduh"
   },
+  "downloadScreenshotTitle": {
+    "message": "Unduh tangkapan layar"
+  },
   "downloadOnlyNotice": {
     "message": "Anda saat ini berada di mode Hanya-Unduh."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots secara otomatis berganti ke mode Hanya-Unduh pada situasi berikut:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "Di jendela Penjelajahan Pribadi."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Salin"
   },
+  "copyScreenshotTitle": {
+    "message": "Salin tangkapan layar ke papan klip"
+  },
   "notificationImageCopiedTitle": {
     "message": "Tangkapan Disalin"
   },
   "notificationImageCopiedDetails": {
     "message": "Tangkapan Anda telah disalin ke papan klip. Tekan $META_KEY$-V untuk menempelkan.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Maaf atas ketidaknyamanannya. Kami sedang mengerjakan fitur ini untuk peluncuran masa mendatang."
   },
   "genericErrorTitle": {
     "message": "Wah! Firefox Screenshots mendadak kacau."
   },
   "genericErrorDetails": {
     "message": "Kami tidak yakin akan apa yang terjadi. Ingin mencoba lagi atau merekam gambar dari laman yang berbeda?"
   },
-  "tourBodyIntro": {
-    "message": "Ambil, simpan, dan bagikan tangkapan layar tanpa meninggalkan Firefox."
+  "tourBodyIntroServerless": {
+    "message": "Ambil, salin, dan unduh tangkapan layar tanpa meninggalkan Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Cara baru untuk menyimpan"
   },
   "tourBodyPageAction": {
     "message": "Bentangkan menu tindakan laman di bilah alamat setiap kali Anda ingin buat tangkapan layar."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Klik dan seret untuk merekam sebagian area laman. Anda juga dapat menggeser kursor untuk menyoroti pilihan Anda."
   },
   "tourHeaderFullPage": {
     "message": "Rekam Jendela atau Seluruh Laman"
   },
   "tourBodyFullPage": {
     "message": "Pilih tombol di kanan atas untuk merekam area yang terlihat pada jendela atau rekam seluruh laman."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Sesuka Anda"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Simpan potongan tangkapan Anda ke Web agar mudah dibagikan, atau unduh ke komputer. Anda pun dapat mengeklik pada tombol Gambar Saya untuk menemukan semua tangkapan yang pernah Anda rekam."
-  },
-  "tourHeaderAccounts": {
-    "message": "Screenshots to Go"
-  },
-  "tourBodyAccounts": {
-    "message": "Masuk dengan Firefox Account untuk mengakses tangkapan Anda di semua peranti Anda dan menyimpan tangkapan favorit Anda selamanya."
-  },
   "tourSkip": {
     "message": "LEWATI"
   },
   "tourNext": {
     "message": "Salindia Selanjutnya"
   },
   "tourPrevious": {
     "message": "Salindia Sebelumnya"
--- a/browser/extensions/screenshots/_locales/ja/messages.json
+++ b/browser/extensions/screenshots/_locales/ja/messages.json
@@ -27,16 +27,19 @@
     "message": "ページ全体を保存"
   },
   "cancelScreenshot": {
     "message": "キャンセル"
   },
   "downloadScreenshot": {
     "message": "ダウンロード"
   },
+  "downloadScreenshotTitle": {
+    "message": "スクリーンショットをダウンロード"
+  },
   "downloadOnlyNotice": {
     "message": "ダウンロード専用モードが有効になっています。"
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots は以下のような状況では自動的にダウンロード専用モードへ切り替わります。"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "プライベートブラウジングを使用している場合。"
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "コピー"
   },
+  "copyScreenshotTitle": {
+    "message": "スクリーンショットをクリップボードにコピー"
+  },
   "notificationImageCopiedTitle": {
     "message": "ショットをコピーしました"
   },
   "notificationImageCopiedDetails": {
     "message": "ショットがクリップボードへコピーされました。$META_KEY$+V キーで貼り付けられます。",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "ご不便をおかけして申し訳ありません。今後のリリースでこの機能を提供できるよう取り組んでいます。"
   },
   "genericErrorTitle": {
     "message": "Firefox Screenshots に問題が発生しました。"
   },
   "genericErrorDetails": {
     "message": "何か問題が発生したようです。再度試すか、別のページのショットを撮ってみてください。"
   },
-  "tourBodyIntro": {
-    "message": "Firefox を離れることなく、スクリーンショットを撮影、保存、共有。"
+  "tourBodyIntroServerless": {
+    "message": "Firefox だけでスクリーンショットの撮影、コピー、ダウンロードができます。"
   },
   "tourHeaderPageAction": {
     "message": "新たな保存方法"
   },
   "tourBodyPageAction": {
     "message": "スクリーンショットを撮りたいときは、いつでもアドレスバー内のページアクションメニューを開いてください。"
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "クリック&ドラッグでページの一部だけをキャプチャできます。また、マウスを当てれば選択範囲が強調表示されます。"
   },
   "tourHeaderFullPage": {
     "message": "ウィンドウもしくはページ全体をキャプチャ"
   },
   "tourBodyFullPage": {
     "message": "右上のボタンを選択して、ウィンドウ内の表示範囲もしくはページ全体をキャプチャしましょう。"
   },
-  "tourHeaderDownloadUpload": {
-    "message": "お好きなように"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "切り取ったショットを簡単に共有できるようウェブ上に保存したり、手元へダウンロードしたり。また「自分のショット」ボタンをクリックすれば、これまでに撮ったすべてのショットを見られます。"
-  },
-  "tourHeaderAccounts": {
-    "message": "Screenshots to Go"
-  },
-  "tourBodyAccounts": {
-    "message": "Firefox アカウントでログインすれば、お持ちのすべての端末からショットにアクセスでき、お気に入りのショットを保存しておけます。"
-  },
   "tourSkip": {
     "message": "スキップ"
   },
   "tourNext": {
     "message": "次のスライド"
   },
   "tourPrevious": {
     "message": "前のスライド"
--- a/browser/extensions/screenshots/_locales/ka/messages.json
+++ b/browser/extensions/screenshots/_locales/ka/messages.json
@@ -27,30 +27,33 @@
     "message": "მთლიანი გვერდის შენახვა"
   },
   "cancelScreenshot": {
     "message": "გაუქმება"
   },
   "downloadScreenshot": {
     "message": "ჩამოტვირთვა"
   },
+  "downloadScreenshotTitle": {
+    "message": "ეკრანის სურათის ჩამოტვირთვა"
+  },
   "downloadOnlyNotice": {
     "message": "თქვენ ახლა იმყოფებით „მხოლოდ ჩამოტვირთვის“ რეჟიმში."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots გადადის „მხოლოდ ჩამოტვირთვის“ რეჟიმზე, შემდეგ შემთხვევებში:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "პირადი ფანჯრით სარგებლობისას."
   },
   "downloadOnlyDetailsThirdParty": {
     "message": "მესამე მხარის ფუნთუშების შენახვა, შეზღუდულია."
   },
   "downloadOnlyDetailsNeverRemember": {
-    "message": "მითითებულია, რომ “არასოდეს დაიმახსოვრებს ისტორიას” ბრაუზერი."
+    "message": "მითითებულია, რომ ბრაუზერი „არასოდეს დაიმახსოვრებს ისტორიას“."
   },
   "downloadOnlyDetailsESR": {
     "message": "თქვენ იყენებთ Firefox ESR-ს."
   },
   "downloadOnlyDetailsNoUploadPref": {
     "message": "ატვირთვა შეზღუდულია."
   },
   "notificationLinkCopiedTitle": {
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "ასლი"
   },
+  "copyScreenshotTitle": {
+    "message": "სურათის ასლის აღება"
+  },
   "notificationImageCopiedTitle": {
     "message": "სურათის ასლი მზადაა"
   },
   "notificationImageCopiedDetails": {
     "message": "თქვენი სურათის ასლი მზადაა. ჩასმისთვის დააწექით $META_KEY$-V.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "ბოდიშს გიხდით გაუგებრობის გამო. ჩვენ ვმუშაობთ ამ შესაძლებლობის დამატებაზე, სამომავლო ვერსიებში."
   },
   "genericErrorTitle": {
     "message": "ვაი! Firefox Screenshots მწყობრიდან გამოვიდა."
   },
   "genericErrorDetails": {
     "message": "გაუგებარია რა მოხდა. ისევ ცდით ხელახლა, თუ სხვა ვებგვერდს გადაუღებთ სურათს?"
   },
-  "tourBodyIntro": {
-    "message": "გადაიღეთ, შეინახეთ და გააზიარეთ ეკრანის სურათები Firefox-იდან გაუსვლელად."
+  "tourBodyIntroServerless": {
+    "message": "გადაიღეთ, გააკეთეთ ასლი და ჩამოტვირთეთ ეკრანის სურათები Firefox-იდან გაუსვლელად."
   },
   "tourHeaderPageAction": {
     "message": "შენახვის ახალი ხერხი"
   },
   "tourBodyPageAction": {
     "message": "როცა მოგესურვებათ ეკრანისთვის სურათის გადაღება, ჩამოშალეთ გვერდზე მოქმედებების მენიუ, რომელიც მდებარეობს მისამართების ველში."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "გადაადგილეთ ან დააწკაპეთ გვერდზე გადასაღები სივრცის შესარჩევად. ასევე, მაჩვენებელი ისრის გადატარებით შეგიძლიათ მონიშნოთ სასურველი არე."
   },
   "tourHeaderFullPage": {
     "message": "გადაუღეთ სურათები ფანჯრებს ან მთლიან ვებგვერდებს"
   },
   "tourBodyFullPage": {
     "message": "მარჯვენა ზედა კუთხეში არსებული ღილაკების საშუალებით, შეგიძლიათ გადაუღოთ სურათი ხილულ ნაწილს ან მთლიან გვერდს."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "როგორც გენებოთ"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "ამოჭრილი სურათები შეგიძლიათ განათავსოთ ინტერნეტში, მარტივად გასაზიარებლად, ან ჩამოტვირთოთ კომპიუტერში. ასევე, „ჩემი გადაღებულების“ ღილაკზე დაწკაპებით, იხილავთ თქვენ მიერ გადაღებულ ყველა სურათს."
-  },
-  "tourHeaderAccounts": {
-    "message": "თან წაიყოლეთ Screenshots"
-  },
-  "tourBodyAccounts": {
-    "message": "შედით Firefox-ანგარიშზე თქვენს გადაღებულ სურათებთან წვდომის მისაღებად ყველა თქვენი მოწყობილობიდან და სასურველი სურათების სამუდამოდ შესანახად."
-  },
   "tourSkip": {
     "message": "გამოტოვება"
   },
   "tourNext": {
     "message": "შემდეგი"
   },
   "tourPrevious": {
     "message": "წინა"
--- a/browser/extensions/screenshots/_locales/kab/messages.json
+++ b/browser/extensions/screenshots/_locales/kab/messages.json
@@ -27,16 +27,19 @@
     "message": "Sekles asebter meṛṛa"
   },
   "cancelScreenshot": {
     "message": "Sefsex"
   },
   "downloadScreenshot": {
     "message": "Sider"
   },
+  "downloadScreenshotTitle": {
+    "message": "Sider tuṭṭfa n ugdil"
+  },
   "downloadOnlyNotice": {
     "message": "Aql-ak tura deg umskar n usider kan."
   },
   "downloadOnlyDetails": {
     "message": "Deg isekaren-agi, Firefox Screenshots ad k-yeǧǧ kan ad tsidreḍ:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "Deg iccer n tungin uslig."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Nγel"
   },
+  "copyScreenshotTitle": {
+    "message": "Nɣel tuṭṭfa ɣef afus"
+  },
   "notificationImageCopiedTitle": {
     "message": "Tuṭṭfa tettwanγel"
   },
   "notificationImageCopiedDetails": {
     "message": "Tuṭṭfa-inek tettwanγel yer ufus. Senned yef $META_KEY$-V akken ad tsenṭḍeḍ.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,19 +126,16 @@
     "message": "Suref-aɣ ɣef aya. Aqlaɣ nxeddem ɣef tmahilt i yileqman d-iteddun."
   },
   "genericErrorTitle": {
     "message": "Ihuh! Firefox Screenshots ur iteddu ara."
   },
   "genericErrorDetails": {
     "message": "Ur neẓri ara acu yeḍran. Ɛreḍ tikelt-nniḍen neɣ ṭṭef agdil n usebter-nniḍen?"
   },
-  "tourBodyIntro": {
-    "message": "Ṭṭef, sekles, bḍu igdilen war ma teffɣeḍ si Firefox."
-  },
   "tourHeaderPageAction": {
     "message": "Abrid amaynut i wsekles"
   },
   "tourBodyPageAction": {
     "message": "Mi tebγiḍ ad teṭṭfeḍ agdil ldi umuγ n tigawin n usebter illan deg ufeggag n tansiwin."
   },
   "tourHeaderClickAndDrag": {
     "message": "Ṭṭef kan ayen tebγiḍ"
@@ -141,28 +144,16 @@
     "message": "Sit sakin zuɣer akken ad teṭṭfeḍ aḥric seg usebter. Tzemreḍ daɣen ad tesrifgeḍ akken ad tsebṛuṛqeḍ afran-ik."
   },
   "tourHeaderFullPage": {
     "message": "Ṭṭef isfuyla neγ isebtar meṛṛa"
   },
   "tourBodyFullPage": {
     "message": "Fren tiqeffalin s afella ayeffus akken ad teṭṭfeḍ tamnaṭ yettbanen deg usfaylu neɣ asebter i meṛṛa."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Akken tebγiḍ"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Sekles tuṭṭfiwin-ik ar Web i beṭṭu fessusen, neɣ sider-itent-id ar uselkim-ik. Tzemr€d daɣen ad tiseḍ ɣef tqeffalt Tiṭṭfiwin-iw akken ad tafeḍ akk tuṭṭfiwin n ugdil i teggid."
-  },
-  "tourHeaderAccounts": {
-    "message": "Tuṭṭfiwin n wegdil ara yeddun"
-  },
-  "tourBodyAccounts": {
-    "message": "Jerred s umiḍan-ik n Firefox akken ad tkecmeḍ ɣer tuṭṭfiwin-inek deg ibenkan-ik meṛṛa  wa ad tkelseḍ tuṭṭfiwin-inek i tḥemleḍ i lebda."
-  },
   "tourSkip": {
     "message": "Zgel"
   },
   "tourNext": {
     "message": "Tigri n zdat"
   },
   "tourPrevious": {
     "message": "Tigri n deffir"
--- a/browser/extensions/screenshots/_locales/kk/messages.json
+++ b/browser/extensions/screenshots/_locales/kk/messages.json
@@ -126,16 +126,19 @@
     "message": "Қолайсыздық үшін кешірім сұраймыз. Бұл мүмкіндікті болашақ шығарылымдарда іске асыруға жұмысты жасаймыз."
   },
   "genericErrorTitle": {
     "message": "Қап! Firefox скриншоттары жасамай қалған сияқты."
   },
   "genericErrorDetails": {
     "message": "Не болғанын білмейміз. Қайталап көресіз бе, немесе басқа парақтың скриншотын түсіріп көресіз бе?"
   },
+  "tourBodyIntroServerless": {
+    "message": "Firefox-тан шықпай-ақ, скриншоттарды түсіріп, көшіріп, жүктеп алыңыз."
+  },
   "tourHeaderPageAction": {
     "message": "Сақтаудың жаңа жолы"
   },
   "tourBodyPageAction": {
     "message": "Скриншотты жасағыңыз келген уақытта адрестік жолақтың бет әрекеттері мәзірін ашыңыз."
   },
   "tourHeaderClickAndDrag": {
     "message": "Тек керек нәрсені түсіріңіз"
--- a/browser/extensions/screenshots/_locales/ml/messages.json
+++ b/browser/extensions/screenshots/_locales/ml/messages.json
@@ -12,48 +12,91 @@
     "message": "എന്റെ ഷോട്ടുകള്‍"
   },
   "screenshotInstructions": {
     "message": "ഒരു പ്രദേശം തിരഞ്ഞെടുക്കാൻ താളില്‍ ഡ്രാഗ് ചെയ്യുക അല്ലെങ്കിൽ ക്ലിക്കുചെയ്യുക. റദ്ദാക്കാൻ ESC അമർത്തുക."
   },
   "saveScreenshotSelectedArea": {
     "message": "സംരക്ഷിക്കുക"
   },
+  "uploadScreenshotSelectedArea": {
+    "message": "അപ്‌ലോഡ്"
+  },
   "saveScreenshotVisibleArea": {
     "message": "കാണുന്നതു സംരക്ഷിക്കുക"
   },
   "saveScreenshotFullPage": {
     "message": "താള്‍ മുഴുവന്‍ സംരക്ഷിക്കുക"
   },
   "cancelScreenshot": {
     "message": "റദ്ദാക്കുക"
   },
   "downloadScreenshot": {
     "message": "ഡൗണ്‍ലോഡ് ചെയ്യുക"
   },
+  "downloadScreenshotTitle": {
+    "message": "സ്ക്രീൻഷോട്ട് ഡൗൺലോഡ് ചെയ്യുക"
+  },
+  "downloadOnlyNotice": {
+    "message": "നിങ്ങൾ ഇപ്പോൾ ഡൗൺലോഡ് മാത്രം ഉള്ള മോഡിൽ ആണ്."
+  },
+  "downloadOnlyDetails": {
+    "message": "ഫയർഫോക്സ് സ്ക്രീൻഷോട്ടുകൾ ഈ സാഹചര്യങ്ങളിൽ ഡൗൺലോഡ്-മാത്രം മോഡിലേക് സ്വയം മാറുന്നു:"
+  },
   "downloadOnlyDetailsPrivate": {
     "message": "സ്വകാര്യ ബ്രൌസിങ്ങ് ജാലകത്തില്‍."
   },
+  "downloadOnlyDetailsThirdParty": {
+    "message": "മൂന്നാം കക്ഷി കുക്കികൾ അപ്രാപ്തമാക്കി."
+  },
+  "downloadOnlyDetailsNeverRemember": {
+    "message": "“ചരിത്രം ഒരിക്കലും ഓർക്കേണ്ട” എന്ന സജ്ജീകരണം പ്രാപ്തമാക്കിയിരിക്കുന്നു."
+  },
+  "downloadOnlyDetailsESR": {
+    "message": "നിങ്ങൾ ഫയർഫോക്സ് ESR ഉപയോഗിക്കുന്നു."
+  },
+  "downloadOnlyDetailsNoUploadPref": {
+    "message": "അപ്ലോഡുകൾ അപ്രാപ്തമാക്കി."
+  },
   "notificationLinkCopiedTitle": {
     "message": "ലിങ്ക് പകര്‍ത്തി"
   },
   "notificationLinkCopiedDetails": {
     "message": "നിങ്ങളുടെ ഷോട്ടിലേക്കുള്ള ലിങ്ക് ക്ലിപ്പ്ബോർഡിലേക്ക് പകർത്തി. ഒട്ടിക്കാൻ $META_KEY$-V അമർത്തുക.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "പകര്‍ത്തുക"
   },
+  "copyScreenshotTitle": {
+    "message": "ക്ലിപ്പ്ബോർഡിലേക്ക് സ്ക്രീൻഷോട്ട് പകർത്തുക"
+  },
   "notificationImageCopiedTitle": {
     "message": "ഷോട്ട് പകര്‍ത്തി"
   },
+  "notificationImageCopiedDetails": {
+    "message": "നിങ്ങളുടെ ഷോട്ടിലേക്കുള്ള ലിങ്ക് ക്ലിപ്പ്ബോർഡിലേക്ക് പകർത്തി. ഒട്ടിക്കാൻ $META_KEY$-V അമർത്തുക.",
+    "placeholders": {
+      "meta_key": {
+        "content": "$1"
+      }
+    }
+  },
+  "imageCropPopupWarning": {
+    "message": "സംരക്ഷിത ചിത്രം $PIXELS$px ഉയരത്തിലേക്ക് മാറ്റപ്പെടും.",
+    "placeholders": {
+      "pixels": {
+        "content": "$1"
+      }
+    }
+  },
   "requestErrorTitle": {
     "message": "പണി പാളി."
   },
   "requestErrorDetails": {
     "message": "ക്ഷമിക്കണം! താങ്കളുടെ സ്ക്രീൻഷോട്ട് സൂക്ഷിക്കാന്‍ കഴിഞ്ഞില്ല. ദയവായി പിന്നീടു ശ്രമിക്കുക."
   },
   "connectionErrorTitle": {
     "message": "നിങ്ങളുടെ സ്ക്രീൻഷോട്ടുകളിലേക്കു ബന്ധിപ്പിക്കാന്‍ കഴിയുന്നില്ല."
@@ -71,43 +114,72 @@
     "message": "ഇതൊരു സാധാരണ താള്‍ അല്ല, അതിനാൽ അതിന്റെ ഒരു സ്ക്രീൻഷോട്ട് എടുക്കാനാകില്ല."
   },
   "selfScreenshotErrorTitle": {
     "message": "സ്ക്രീൻഷോട്ട് താളിന്റെ സ്ക്രീൻഷോട്ട് എടുക്കാന്‍ പറ്റില്ല‌!"
   },
   "emptySelectionErrorTitle": {
     "message": "നിങ്ങളുടെ തെരഞ്ഞെടുപ്പ് തീരെ ചെറുതാണ്"
   },
+  "privateWindowErrorTitle": {
+    "message": "സ്വകാര്യ ബ്രൗസിംഗ് മോഡിൽ സ്ക്രീൻഷോട്ട്സ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു"
+  },
   "privateWindowErrorDetails": {
     "message": "അസൗകര്യം നേരിടേണ്ടി വന്നതിൽ ഖേദിക്കുന്നു. ഈ സവിശേഷതയുള്ള ഭാവിയിലെ റിലീസുകൾക്കായി ഞങ്ങൾ പ്രവർത്തിക്കുന്നു."
   },
   "genericErrorTitle": {
     "message": "അയ്യോ! ഫയര്‍ഫോക്സ് സ്ക്രീൻഷോട്ടിനു എന്തോ പറ്റി."
   },
   "genericErrorDetails": {
     "message": "എന്താ സംഭവിച്ചതെന്ന് വല്യ പിടി ഇല്ല. ഒന്നുകൂടി നോക്കുകയോ അല്ലെങ്കില്‍ വേറെ താളിന്റെ സ്ക്രീൻഷോട്ട് എടുക്കുകയോ ചെയ്യുന്നോ?"
   },
+  "tourBodyIntroServerless": {
+    "message": "ഫയര്‍ഫോക്സ് വിട്ടുപോകാതെ തന്നെ സ്ക്രീൻഷോട്ടുകൾ എടുക്കുക, പകര്‍ത്തുക, ഡൌണ്‍ലോഡു് ചെയ്യുക."
+  },
   "tourHeaderPageAction": {
     "message": "സൂക്ഷിക്കാന്‍ പുതിയൊരു മാർഗ്ഗം"
   },
-  "tourHeaderDownloadUpload": {
-    "message": "നിങ്ങളുടെ ഇഷ്ടാനുസൃതം"
+  "tourBodyPageAction": {
+    "message": "നിങ്ങൾ സ്ക്രീൻഷോട്ട് എടുക്കാൻ ആഗ്രഹിക്കുമ്പോള്‍ വിലാസ ബാറിലെ പേജ് പ്രവർത്തനങ്ങളുടെ മെനു വിപുലീകരിക്കുക."
+  },
+  "tourHeaderClickAndDrag": {
+    "message": "നിങ്ങൾക്ക് വേണ്ടതു് പിടിച്ചെടുക്കുക"
+  },
+  "tourBodyClickAndDrag": {
+    "message": "ഒരു പേജിന്റെ ഒരു ഭാഗം പകർത്താൻ ക്ലിക്കുചെയ്ത് ഇഴയ്ക്കുക. നിങ്ങളുടെ തിരഞ്ഞെടുക്കൽ ഹൈലൈറ്റുചെയ്ത് നിങ്ങൾക്ക് ഹോവർ ചെയ്യാനാകും."
+  },
+  "tourHeaderFullPage": {
+    "message": "പണിയിടം അല്ലെങ്കിൽ പേജു് മുഴുവൻ ക്യാപ്ചർ ചെയ്യുക"
+  },
+  "tourBodyFullPage": {
+    "message": "പണിയിടത്തില്‍ ദൃശ്യമായ പ്രദേശം പിടിച്ചെടുക്കാനോ അല്ലെങ്കിൽ ഒരു മുഴുവൻ പേജ് എടുക്കാനോ മുകളിലോ വലതുഭാഗത്തുള്ള ബട്ടണുകൾ തിരഞ്ഞെടുക്കുക."
   },
   "tourSkip": {
     "message": "ഒഴിവാക്കുക"
   },
   "tourNext": {
     "message": "അടുത്ത സ്ലൈഡ്"
   },
   "tourPrevious": {
     "message": "മുമ്പത്തെ സ്ലൈഡ്"
   },
   "tourDone": {
     "message": "തീര്‍ന്നു"
   },
+  "termsAndPrivacyNotice2": {
+    "message": "ഫയർഫോക്സ് സ്ക്രീൻഷോട്ട്സ് ഉപയോഗിക്കുമ്പോൾ, നിങ്ങൾ ഞങ്ങളുടെ $TERMSANDPRIVACYNOTICETERMSLINK$, $TERMSANDPRIVACYNOTICEPRIVACYLINK$ എന്നിവ അംഗീകരിക്കുന്നു.",
+    "placeholders": {
+      "termsandprivacynoticetermslink": {
+        "content": "$1"
+      },
+      "termsandprivacynoticeprivacylink": {
+        "content": "$2"
+      }
+    }
+  },
   "termsAndPrivacyNoticeTermsLink": {
     "message": "നിബന്ധനകൾ"
   },
   "termsAndPrivacyNoticyPrivacyLink": {
     "message": "സ്വകാര്യതാ അറിയിപ്പ്"
   },
   "libraryLabel": {
     "message": "സ്ക്രീൻഷോട്ടുകൾ"
--- a/browser/extensions/screenshots/_locales/my/messages.json
+++ b/browser/extensions/screenshots/_locales/my/messages.json
@@ -27,16 +27,19 @@
     "message": "စာမျက်နှာတစ်ခုလုံးကို သိမ်းပါ"
   },
   "cancelScreenshot": {
     "message": "မဆောင်ရွက်တော့ပါ"
   },
   "downloadScreenshot": {
     "message": "ဆွဲယူရန်"
   },
+  "downloadScreenshotTitle": {
+    "message": "မျက်နှာပြင်ပုံဖမ်းချက်ကို ကူးယူဆွဲချပါ"
+  },
   "downloadOnlyNotice": {
     "message": "ဆွဲချမှု တစ်မျိုးသာသုံးသော ပုံစံဖြင့် သင်အခုသုံးနေသည်"
   },
   "downloadOnlyDetails": {
     "message": "ဒီအခြေအနေတွင် Firefoxမှ ရိုက်ချက်များသည် အလိုအလျောက် ဆွဲချမှုတစ်မျိုးတည်းသာသုံးသောပုံစံသို့ ပြောင်းလဲသည်"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "သီးသန့်ကြည့်ရှုခြင်းပုံစံ"
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "ကူးပါ"
   },
+  "copyScreenshotTitle": {
+    "message": "မျက်နှာပြင်ပုံဖမ်းချက်ကို ကလစ်ဘုတ်သို့ ကူးယူပါ"
+  },
   "notificationImageCopiedTitle": {
     "message": "ရိုက်ချက်ကူးပါ"
   },
   "notificationImageCopiedDetails": {
     "message": "သင်ဖမ်းယူခဲ့သော ပုံကို ကလစ်ဘုတ်သို့ ကူးယူပြီးပြီ။ ပွားယူရန် $META_KEY$-V ကို နှိပ်ပါ။",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "အဆင်မပြေမှုများအတွက် တောင်းပန်ပါတယ်။ ယခုလုပ်ဆောင်ချက်ကို နောင်ထုတ်ကုန်တွင် ပါဝင်စေရန် ဆောင်ရွက်နေပါသည်။"
   },
   "genericErrorTitle": {
     "message": "ဝိုး။ Firefox Screenshots မှာ အမှားဖြစ်ပေါ်ခဲ့သည်။"
   },
   "genericErrorDetails": {
     "message": "ဘာဖြစ်သွားခဲ့မှန်း သေချာမသိခဲ့ပါ။ ထပ်စမ်းကြည့်လိုပါသလား သို့မဟုတ် အခြားဝဘ်စာမျက်နှာကို ပုံရိပ်ဖမ်းလိုပါသလား။"
   },
-  "tourBodyIntro": {
-    "message": "Firefox ကနေ ထွက်ခွာရန် မလိုဘဲ မျက်နှာပြင်ပုံရိပ်များကို ရိုက်ကူး၊ သိမ်းဆည်း၊ မျှဝေပါ။"
+  "tourBodyIntroServerless": {
+    "message": "Firefox ကို ပိတ်စရာမလိုပဲ မျက်နှာပြင်ပုံဖမ်းချက်များကို ရိုက်ကူး၊ ပွားယူ၊ ဆွဲချပါ။"
   },
   "tourHeaderPageAction": {
     "message": "သိမ်းဆဲရန် နည်းလမ်းအသစ်"
   },
   "tourBodyPageAction": {
     "message": "သင် မျက်နှာပြင်ပုံရိပ်ဖမ်းလိုသည့် အခါတိုင်း လိပ်စာဘားတန်းရှိ စာမျက်နှာလုပ်ဆောင်ချက်များ မီနူးကို ဖြန့်ချပါ။"
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "စာမျက်နှာ၏ အစိတ်အပိုင်းကို ဖမ်းယူရန် ကလစ်နှိပ်ပြီး ဖိဆွဲပါ။ သင့်ရွေးချယ်မှုကို ထင်ရှားစေရန် ညွှန်တံမြားကို ဆိုင်ရာအစိတ်အပိုင်းပေါ် ရွှေ့နိုင်သည်။"
   },
   "tourHeaderFullPage": {
     "message": "ဝင်ဒိုးများ သို့မဟုတ် စာမျက်နှာတစ်ခုလုံးကို ဖမ်းယူပါ"
   },
   "tourBodyFullPage": {
     "message": "ဝင်းဒိုးထဲရှိ မြင်ရသော အကျယ်အဝန်းကို ဖမ်းယူရန် သို့မဟုတ် စာမျက်နှာတစ်ခုလုံးကို ဖမ်းယူရန် ညာဘက်အပေါ်ဘက်ရှိ ခလုတ်များကို ရွေးပါ။"
   },
-  "tourHeaderDownloadUpload": {
-    "message": "နှစ်သက်သလို"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "ဝဘ်တွင် အလွယ်တကူ မျှဝေရန် သို့မဟုတ် ကွန်ပျူတာထဲသို့ ဆွဲယူကူးရန် ဖြတ်တောက်ထားသော ပုံဖမ်းချက်များကို သိမ်းပါ။ ရိုက်ထားသမျှပုံများအားလုံးကို ရှာဖွေကြည့်ရှုရန် ရိုက်ထားသောပုံများတွင်လည်း ကလစ်နှိပ်ကြည့်နိုင်သည်။"
-  },
-  "tourHeaderAccounts": {
-    "message": "အသုံးပြုတိုင်း ရှိနေမည့် Screenshots"
-  },
-  "tourBodyAccounts": {
-    "message": "ကိရိယာအားလုံးရှိ မျက်နှာပြင်ရိုက်ချက်များကို အသုံးပြုရန် နှင့် သင်နှစ်သက်သည်များကို သိမ်းဆည်းရန် သင့် Firefox Account နှင့် ဝင်ရောက်ပါ။"
-  },
   "tourSkip": {
     "message": "SKIP"
   },
   "tourNext": {
     "message": "နောက်ဆလိုက်"
   },
   "tourPrevious": {
     "message": "အရင်ကဆလိုက်"
--- a/browser/extensions/screenshots/_locales/nn_NO/messages.json
+++ b/browser/extensions/screenshots/_locales/nn_NO/messages.json
@@ -27,16 +27,19 @@
     "message": "Lagre heile sida"
   },
   "cancelScreenshot": {
     "message": "Avbryt"
   },
   "downloadScreenshot": {
     "message": "Last ned"
   },
+  "downloadScreenshotTitle": {
+    "message": "Last ned skjermbildet"
+  },
   "downloadOnlyNotice": {
     "message": "Du er for tida i nedlastingsmodus."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots endrar automatisk til nedlastingsmodus i følgjande situasjonar:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "I eit privat nettlesingsvindauge."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Kopier"
   },
+  "copyScreenshotTitle": {
+    "message": "Kopier skjermbildet til utklippstavla"
+  },
   "notificationImageCopiedTitle": {
     "message": "Bilde kopiert"
   },
   "notificationImageCopiedDetails": {
     "message": "Bildet ditt er kopiert til utklippstavla. Trykk på $META_KEY$-V for å lime det inn.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Ein er lei for ulempa. Vi jobbar med denne funksjonen for framtidige versjonar."
   },
   "genericErrorTitle": {
     "message": "Oj! Det ser ut til at Firefox Screenshots ikkje fungerer korrekt."
   },
   "genericErrorDetails": {
     "message": "Vi er ikkje sikre på kva som hende. Kan du prøve igjen eller ta eit bilde på ei anna side?"
   },
-  "tourBodyIntro": {
-    "message": "Ta, lagre og del skjermbilde utan å forlate Firefox."
+  "tourBodyIntroServerless": {
+    "message": "Ta, kopier og last ned skjermbilde utan å forlate Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Ein ny måte å lagre på"
   },
   "tourBodyPageAction": {
     "message": "Utvid sidehandlingsmenyen i adresselinja når du vil ta eit skjermbilde."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Klikk for å drage og knipse berre ein del av sida. Du kan også føre musa over for å framheve merkt område."
   },
   "tourHeaderFullPage": {
     "message": "Knips vindauge eller heile sider"
   },
   "tourBodyFullPage": {
     "message": "Vel knappane i det øvre høgre hjørnet for å knipse det synlege området i vindauget eller for å knipse ei heil side."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Som du vil ha det"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Lagre dei tilskorne bilda dine på nettet for enklare deling, eller last dei ned til datamaskina di. Du kan også klikke på knappen Mine skjermbilde for å finne alle bilda du har tatt."
-  },
-  "tourHeaderAccounts": {
-    "message": "Skjermbilde til å ta med"
-  },
-  "tourBodyAccounts": {
-    "message": "Logg på med Firefox-kontoen din for å få tilgang til bilda dine på alle einingane dine og lagre favorittbilda dine for alltid."
-  },
   "tourSkip": {
     "message": "Hopp over"
   },
   "tourNext": {
     "message": "Neste slide"
   },
   "tourPrevious": {
     "message": "Føregåande slide"
@@ -177,17 +171,17 @@
         "content": "$1"
       },
       "termsandprivacynoticeprivacylink": {
         "content": "$2"
       }
     }
   },
   "termsAndPrivacyNoticeTermsLink": {
-    "message": "Vilkår"
+    "message": "vilkåra"
   },
   "termsAndPrivacyNoticyPrivacyLink": {
-    "message": "Personvernmerknad"
+    "message": "personvernmerknaden"
   },
   "libraryLabel": {
     "message": "Skjermbilde"
   }
 }
\ No newline at end of file
--- a/browser/extensions/screenshots/_locales/pt_BR/messages.json
+++ b/browser/extensions/screenshots/_locales/pt_BR/messages.json
@@ -7,17 +7,17 @@
   },
   "contextMenuLabel": {
     "message": "Capturar tela"
   },
   "myShotsLink": {
     "message": "Minhas capturas"
   },
   "screenshotInstructions": {
-    "message": "Arraste ou clique na página para selecionar uma região. Pressione ESC para cancelar."
+    "message": "Clique e arraste ou aponte e clique para selecionar uma região. Tecle ESC para cancelar."
   },
   "saveScreenshotSelectedArea": {
     "message": "Salvar"
   },
   "uploadScreenshotSelectedArea": {
     "message": "Enviar"
   },
   "saveScreenshotVisibleArea": {
@@ -127,29 +127,29 @@
   },
   "genericErrorTitle": {
     "message": "Uoou! O Firefox Screenshot enlouqueceu."
   },
   "genericErrorDetails": {
     "message": "Não temos certeza do que acabou de acontecer. Tentar novamente ou fazer uma captura de uma página diferente?"
   },
   "tourBodyIntroServerless": {
-    "message": "Faça, copie e baixe capturas de tela sem deixar o Firefox."
+    "message": "Capture telas, copie e baixe sem sair do Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Um novo jeito de salvar"
   },
   "tourBodyPageAction": {
-    "message": "Abra o menu de ações da página na barra de endereços sempre que quiser fazer uma captura de tela."
+    "message": "Abra o menu de ações da página na barra de endereços sempre que quiser capturar uma tela."
   },
   "tourHeaderClickAndDrag": {
     "message": "Capture apenas o que você quer"
   },
   "tourBodyClickAndDrag": {
-    "message": "Clique e arraste para capturar apenas uma parte de uma página. Você também pode passar o mouse para realçar sua seleção."
+    "message": "Clique e arraste para capturar apenas uma parte da página. Você também pode mover o mouse sobre a tela para realçar uma seleção."
   },
   "tourHeaderFullPage": {
     "message": "Capture janelas ou páginas inteiras"
   },
   "tourBodyFullPage": {
     "message": "Selecione os botões no canto superior direito para capturar a área visível na janela ou capturar uma página inteira."
   },
   "tourSkip": {
--- a/browser/extensions/screenshots/_locales/pt_PT/messages.json
+++ b/browser/extensions/screenshots/_locales/pt_PT/messages.json
@@ -19,17 +19,17 @@
   },
   "uploadScreenshotSelectedArea": {
     "message": "Carregar"
   },
   "saveScreenshotVisibleArea": {
     "message": "Guardar visível"
   },
   "saveScreenshotFullPage": {
-    "message": "Guardar página inteira"
+    "message": "Guardar página completa"
   },
   "cancelScreenshot": {
     "message": "Cancelar"
   },
   "downloadScreenshot": {
     "message": "Transferir"
   },
   "downloadScreenshotTitle": {
--- a/browser/extensions/screenshots/_locales/sk/messages.json
+++ b/browser/extensions/screenshots/_locales/sk/messages.json
@@ -27,16 +27,19 @@
     "message": "Uložiť celú stránku"
   },
   "cancelScreenshot": {
     "message": "Zrušiť"
   },
   "downloadScreenshot": {
     "message": "Prevziať"
   },
+  "downloadScreenshotTitle": {
+    "message": "Prevziať snímku obrazovky"
+  },
   "downloadOnlyNotice": {
     "message": "Ste v režime len na prevzatie."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots automaticky prejde do režimu len na prevzatie v nasledujúcich situáciách:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "Ste v režime Súkromné prehliadanie."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Kopírovať"
   },
+  "copyScreenshotTitle": {
+    "message": "Skopírovať snímok do schránky"
+  },
   "notificationImageCopiedTitle": {
     "message": "Snímka bola skopírovaná"
   },
   "notificationImageCopiedDetails": {
     "message": "Vaša snímka bola skopírovaná do schránky. Stlačením $META_KEY$-V ju prilepíte.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Ospravedlňujeme sa za spôsobené nepríjemnosti. Pracujeme na vylepšení tejto funkcie v budúcich verziách."
   },
   "genericErrorTitle": {
     "message": "Ups! Služba Firefox Screenshots prestala pracovať."
   },
   "genericErrorDetails": {
     "message": "Nie sme si istí, čo sa práve stalo. Chcete tú skúsiť znova alebo chcete vytvoriť snímku inej stránky?"
   },
-  "tourBodyIntro": {
-    "message": "Tvorte, ukladajte a zdieľajte snímky obrazovky bez toho, aby ste museli opustiť Firefox."
+  "tourBodyIntroServerless": {
+    "message": "Tvorte, kopírujte a preberajte snímky obrazovky bez toho, aby ste museli opustiť Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Nový spôsob ukladania"
   },
   "tourBodyPageAction": {
     "message": "Kedykoľvek chcete urobiť snímku, otvorte ponuku akcii stránky v paneli s adresou."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Ak chcete zachytiť časť stránky, urobíte to kliknutím a potiahnutím. Váš výber zvýrazníte tak, že sa naň presuniete myšou."
   },
   "tourHeaderFullPage": {
     "message": "Zachyťte okná alebo celé webové stránky"
   },
   "tourBodyFullPage": {
     "message": "Kliknutím na tlačidlo v pravom hornom rohu môžete zachytiť viditeľnú časť stránky. Pomocou ďalšieho tlačidla zachytíte celú stránku."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Urobte to, čo chcete"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Uložte si orezanú snímku na web, aby ste ju mohli ľahšie zdieľať alebo si ju prevziať do počítača. Môžete si taktiež pozrieť všetky vaše snímky - stačí, ak kliknete na tlačidlo Moje snímky."
-  },
-  "tourHeaderAccounts": {
-    "message": "Snímky stránok vždy so sebou"
-  },
-  "tourBodyAccounts": {
-    "message": "Prihláste sa so svojím účtom Firefox a majte prístup ku všetkým svojich snímkam zo všetkých vašich zariadení."
-  },
   "tourSkip": {
     "message": "Preskočiť"
   },
   "tourNext": {
     "message": "Ďalšia snímka"
   },
   "tourPrevious": {
     "message": "Predchádzajúca snímka"
--- a/browser/extensions/screenshots/_locales/sl/messages.json
+++ b/browser/extensions/screenshots/_locales/sl/messages.json
@@ -27,16 +27,19 @@
     "message": "Shrani celotno stran"
   },
   "cancelScreenshot": {
     "message": "Prekliči"
   },
   "downloadScreenshot": {
     "message": "Prenesi"
   },
+  "downloadScreenshotTitle": {
+    "message": "Prenesi posnetek zaslona"
+  },
   "downloadOnlyNotice": {
     "message": "Trenutno ste v načinu samo za prenos."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots se samodejno preklopi v način samo za prenos v naslednjih primerih:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "V oknu zasebnega brskanja."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Kopiraj"
   },
+  "copyScreenshotTitle": {
+    "message": "Kopiraj posnetek zaslona v odložišče"
+  },
   "notificationImageCopiedTitle": {
     "message": "Posnetek kopiran"
   },
   "notificationImageCopiedDetails": {
     "message": "Posnetek zaslona je bil kopiran na odložišče. Pritisnite $META_KEY$-V, da ga prilepite.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Oprostite za nevšečnost. To možnost izboljšujemo za prihodnje izdaje."
   },
   "genericErrorTitle": {
     "message": "Uf! Firefox Screenshots se je pokvaril."
   },
   "genericErrorDetails": {
     "message": "Ne vemo točno, kaj se je pravkar zgodilo. Bi radi poskusili znova ali pa zajeli posnetek kakšne druge strani?"
   },
-  "tourBodyIntro": {
-    "message": "Zajemite, shranite in delite zaslonske posnetke, ne da bi zapustili Firefox."
+  "tourBodyIntroServerless": {
+    "message": "Zajemite, kopirajte in delite zaslonske posnetke, ne da bi zapustili Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Nov način shranjevanja"
   },
   "tourBodyPageAction": {
     "message": "Kadarkoli želite zajeti posnetek zaslona, v naslovni vrstici razširite meni dejanj strani."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Kliknite in povlecite, če želite zajeti samo del strani. Svojo izbiro lahko tudi poudarite, tako da preko nje povlečete miškin kazalec."
   },
   "tourHeaderFullPage": {
     "message": "Zajemite okna ali celotne strani"
   },
   "tourBodyFullPage": {
     "message": "V zgornjem desnem kotu izberite gumb za zajem vidnega območja v oknu ali celotne strani."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Kot vi želite"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Shranite obrezane posnetke na splet za lažje deljenje ali jih prenesite na svoj računalnik. Vse zajete posnetke lahko najdete s klikom na gumb Moji posnetki."
-  },
-  "tourHeaderAccounts": {
-    "message": "Posnetki za na pot"
-  },
-  "tourBodyAccounts": {
-    "message": "Prijavite se s Firefox Računom za dostop do posnetkov na vseh svojih napravah in trajno shranjevanje priljubljenih posnetkov."
-  },
   "tourSkip": {
     "message": "Preskoči"
   },
   "tourNext": {
     "message": "Naslednji diapozitiv"
   },
   "tourPrevious": {
     "message": "Prejšnji diapozitiv"
--- a/browser/extensions/screenshots/_locales/sq/messages.json
+++ b/browser/extensions/screenshots/_locales/sq/messages.json
@@ -27,16 +27,19 @@
     "message": "Ruaj krejt faqen"
   },
   "cancelScreenshot": {
     "message": "Anuloje"
   },
   "downloadScreenshot": {
     "message": "Shkarkoje"
   },
+  "downloadScreenshotTitle": {
+    "message": "Shkarkojeni foton e ekranit"
+  },
   "downloadOnlyNotice": {
     "message": "Gjendeni nën mënyrën Vetëm Shkarkim."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots kalon vetvetiu nën mënyrën Vetëm Shkarkim në këto situata:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "Në një dritare Shfletimi Privat."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Kopjoje"
   },
+  "copyScreenshotTitle": {
+    "message": "Kopjojeni foton e ekranit te e papastra"
+  },
   "notificationImageCopiedTitle": {
     "message": "Fotoja u Kopjua"
   },
   "notificationImageCopiedDetails": {
     "message": "Fotoja juaj u kopjua në të papastër. Për ta ngjitur diku, shtypni $META_KEY$-V.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Na ndjeni për mungesën. Po punojmë mbi këtë veçori për hedhjet e ardhshme në qarkullim."
   },
   "genericErrorTitle": {
     "message": "Yhaaa! Firefox Screenshots shkalloi."
   },
   "genericErrorDetails": {
     "message": "S’jemi të sigurt se ç’ndodhi. Ju prish punë të bëni një foto të një faqeje tjetër?"
   },
-  "tourBodyIntro": {
-    "message": "Bëni, ruani dhe ndani foto ekrani me të tjerët pa dalë nga Firefox-i."
+  "tourBodyIntroServerless": {
+    "message": "Bëni, kopjoni, dhe shkarkoni foto ekrani pa dalë nga Firefox-i."
   },
   "tourHeaderPageAction": {
     "message": "Një mënyrë e re për të ruajtur"
   },
   "tourBodyPageAction": {
     "message": "Kurdo që doni të bëni një foto ekrani, zgjeroni menunë e veprimeve mbi faqen, te shtylla e adresave."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Klikoni dhe tërhiqeni që të fotografoni thjesht një copë të faqes. Mund edhe të kaloni kursorin përsipër që të theksoni përzgjedhjen tuaj."
   },
   "tourHeaderFullPage": {
     "message": "Fiksoni Dritare ose Krejt Faqet"
   },
   "tourBodyFullPage": {
     "message": "Përzgjidhni butonat në cepin e sipërm djathtas që të fotografoni zonën e dukshme te dritarja ose një faqe të tërë."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Si T’ju Pëlqejë"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Ruajini fotot tuaja në web, për ndarje më të lehtë me të tjerët, ose shkarkojini në kompjuterin tuaj. Mund edhe të klikoni te butoni Shkrepjet e Mia që të gjeni krejt shkrepjet që keni bërë."
-  },
-  "tourHeaderAccounts": {
-    "message": "Foto ekrani Kudo Me Vete"
-  },
-  "tourBodyAccounts": {
-    "message": "Hyni në Llogarinë tuaj Firefox që të përdorni shkrepjet tuaja në krejt pajisjet tuaja dhe t’i ruani shkrepjet e parapëlqyera përgjithmonë."
-  },
   "tourSkip": {
     "message": "ANASHKALOJE"
   },
   "tourNext": {
     "message": "Diapozitivi Pasues"
   },
   "tourPrevious": {
     "message": "Diapozitivi i Mëparshëm"
--- a/browser/extensions/screenshots/_locales/th/messages.json
+++ b/browser/extensions/screenshots/_locales/th/messages.json
@@ -27,16 +27,19 @@
     "message": "บันทึกเต็มหน้า"
   },
   "cancelScreenshot": {
     "message": "ยกเลิก"
   },
   "downloadScreenshot": {
     "message": "ดาวน์โหลด"
   },
+  "downloadScreenshotTitle": {
+    "message": "ดาวน์โหลดภาพหน้าจอ"
+  },
   "downloadOnlyNotice": {
     "message": "คุณกำลังอยู่ในโหมดดาวน์โหลดเท่านั้น"
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots จะเปลี่ยนเป็นโหมดดาวน์โหลดอย่างเดียวโดยอัตโนมัติในสถานการณ์เหล่านี้:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "ในหน้าต่างการท่องเว็บแบบส่วนตัว"
@@ -62,27 +65,38 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "คัดลอก"
   },
+  "copyScreenshotTitle": {
+    "message": "คัดลอกภาพหน้าจอไปยังคลิปบอร์ด"
+  },
   "notificationImageCopiedTitle": {
     "message": "คัดลอกภาพหน้าจอแล้ว"
   },
   "notificationImageCopiedDetails": {
     "message": "คัดลอกภาพของคุณไปยังคลิปบอร์ดแล้ว กด $META_KEY$-V เพื่อวาง",
     "placeholders": {
       "meta_key": {
         "content": "$1"
       }
     }
   },
+  "imageCropPopupWarning": {
+    "message": "ภาพที่บันทึกจะถูกครอบตัดให้มีความสูง $PIXELS$ พิกเซล",
+    "placeholders": {
+      "pixels": {
+        "content": "$1"
+      }
+    }
+  },
   "requestErrorTitle": {
     "message": "ใช้งานไม่ได้"
   },
   "requestErrorDetails": {
     "message": "ขออภัย! เราไม่สามารถบันทึกภาพหน้าจอของคุณ โปรดลองอีกครั้งในภายหลัง"
   },
   "connectionErrorTitle": {
     "message": "เราไม่สามารถเชื่อมต่อกับภาพหน้าจอของคุณ"
@@ -112,18 +126,18 @@
     "message": "ขออภัยในความไม่สะดวก เรากำลังพัฒนาคุณลักษณะนี้สำหรับรุ่นในอนาคต"
   },
   "genericErrorTitle": {
     "message": "โอ๊ย! Firefox Screenshots รวน"
   },
   "genericErrorDetails": {
     "message": "เราไม่แน่ใจว่าเกิดอะไรขึ้น ต้องการลองอีกครั้งหรือจับภาพหน้าจอของหน้าอื่น?"
   },
-  "tourBodyIntro": {
-    "message": "จับ บันทึก และแบ่งปันภาพหน้าจอโดยไม่ต้องออกจาก Firefox"
+  "tourBodyIntroServerless": {
+    "message": "จับ คัดลอก และดาวน์โหลดภาพหน้าจอโดยไม่ต้องออกจาก Firefox"
   },
   "tourHeaderPageAction": {
     "message": "หนทางใหม่ในการบันทึก"
   },
   "tourBodyPageAction": {
     "message": "ขยายเมนูการกระทำหน้าในแถบที่อยู่ทุกครั้งที่คุณต้องการจับภาพหน้าจอ"
   },
   "tourHeaderClickAndDrag": {
@@ -133,22 +147,16 @@
     "message": "คลิกแล้วลากเพื่อจับภาพแค่บางส่วนของหน้า คุณยังสามารถวางเมาส์เพื่อเน้นการเลือกของคุณ"
   },
   "tourHeaderFullPage": {
     "message": "จับภาพหน้าต่างหรือทั้งหน้า"
   },
   "tourBodyFullPage": {
     "message": "คลิกที่ปุ่มด้านบนขวาเพื่อจับภาพพื้นที่ที่มองเห็นในหน้าต่างหรือเพื่อจับภาพทั้งหน้า"
   },
-  "tourHeaderDownloadUpload": {
-    "message": "ตามใจชอบ"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "บันทึกภาพหน้าจอที่ครอบตัดของคุณไปยังเว็บเพื่อการแบ่งปันที่ง่ายขึ้น หรือดาวน์โหลดไปยังคอมพิวเตอร์ของคุณ คุณยังสามารถคลิกที่ปุ่ม ภาพหน้าจอของฉัน เพื่อค้นหาภาพหน้าจอทั้งหมดที่คุณได้จับไว้"
-  },
   "tourSkip": {
     "message": "ข้าม"
   },
   "tourNext": {
     "message": "ภาพนิ่งถัดไป"
   },
   "tourPrevious": {
     "message": "ภาพนิ่งก่อนหน้า"
--- a/browser/extensions/screenshots/_locales/uk/messages.json
+++ b/browser/extensions/screenshots/_locales/uk/messages.json
@@ -27,16 +27,19 @@
     "message": "Зберегти всю сторінку"
   },
   "cancelScreenshot": {
     "message": "Скасувати"
   },
   "downloadScreenshot": {
     "message": "Завантажити"
   },
+  "downloadScreenshotTitle": {
+    "message": "Завантажити знімки екрану"
+  },
   "downloadOnlyNotice": {
     "message": "Ви зараз в режимі лише завантаження."
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots автоматично переходить в режим лише завантаження в таких випадках:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "У вікні приватного перегляду."
@@ -62,16 +65,19 @@
       "meta_key": {
         "content": "$1"
       }
     }
   },
   "copyScreenshot": {
     "message": "Копіювати"
   },
+  "copyScreenshotTitle": {
+    "message": "Копіювати знімки в буфер обміну"
+  },
   "notificationImageCopiedTitle": {
     "message": "Знімок скопійовано"
   },
   "notificationImageCopiedDetails": {
     "message": "Ваш знімок був скопійований в буфер обміну. Натисніть $META_KEY$-V, щоб вставити.",
     "placeholders": {
       "meta_key": {
         "content": "$1"
@@ -120,18 +126,18 @@
     "message": "Вибачте за незручності. Ми працюємо над цією функцією для майбутніх випусків."
   },
   "genericErrorTitle": {
     "message": "Оу! З Firefox Screenshots щось негаразд."
   },
   "genericErrorDetails": {
     "message": "Ми не впевнені, в чому проблема. Спробувати ще раз, або ж зробити знімок іншої сторінки?"
   },
-  "tourBodyIntro": {
-    "message": "Робіть знімки екрану, зберігайте та діліться ними прямо в Firefox."
+  "tourBodyIntroServerless": {
+    "message": "Створюйте, копіюйте і завантажуйте знімки екрану, не покидаючи Firefox."
   },
   "tourHeaderPageAction": {
     "message": "Новий спосіб збереження"
   },
   "tourBodyPageAction": {
     "message": "Розгорніть меню дій для сторінки в панелі адреси, коли ви хочете зробити знімок екрану."
   },
   "tourHeaderClickAndDrag": {
@@ -141,28 +147,16 @@
     "message": "Клацніть і потягніть мишею для захоплення частини сторінки. Ви також можете навести курсор миші для підсвічення вибраної області."
   },
   "tourHeaderFullPage": {
     "message": "Захоплюйте вікна або цілі сторінки"
   },
   "tourBodyFullPage": {
     "message": "За допомогою кнопок у верхній правій частині обирайте захоплення видимої області вікна, або сторінки повністю."
   },
-  "tourHeaderDownloadUpload": {
-    "message": "Як вам подобається"
-  },
-  "tourBodyDownloadUpload": {
-    "message": "Зберігайте свої знімки в Інтернеті, щоб легко ними ділитися, або завантажуйте їх на свій комп'ютер. Ви також можете переглянути всі збережені знімки, натиснувши на кнопку Мої знімки."
-  },
-  "tourHeaderAccounts": {
-    "message": "Знімки екрану на ходу"
-  },
-  "tourBodyAccounts": {
-    "message": "Увійдіть в обліковий запис Firefox, щоб отримати доступ до своїх знімків на всіх пристроях і зберігати обрані знімки без обмежень."
-  },
   "tourSkip": {
     "message": "Пропустити"
   },
   "tourNext": {
     "message": "Наступний слайд"
   },
   "tourPrevious": {
     "message": "Попередній слайд"
--- a/browser/extensions/screenshots/_locales/vi/messages.json
+++ b/browser/extensions/screenshots/_locales/vi/messages.json
@@ -40,17 +40,17 @@
   },
   "downloadOnlyDetails": {
     "message": "Firefox Screenshots sẽ tự động chuyển sang chế độ chỉ tải về trong các tình huống:"
   },
   "downloadOnlyDetailsPrivate": {
     "message": "Trong một cửa sổ duyệt web riêng tư."
   },
   "downloadOnlyDetailsThirdParty": {
-    "message": "Cookies của bên thứ ba đã bị vô hiệu hóa."
+    "message": "Cookie của bên thứ ba đã bị vô hiệu hóa."
   },
   "downloadOnlyDetailsNeverRemember": {
     "message": "“Không bao giờ ghi nhớ lược sử” đã được kích hoạt."
   },
   "downloadOnlyDetailsESR": {
     "message": "Bạn đang sử dụng Firefox ESR."
   },
   "downloadOnlyDetailsNoUploadPref": {
@@ -142,23 +142,23 @@
   },
   "tourHeaderClickAndDrag": {
     "message": "Chỉ chụp những gì bạn muốn"
   },
   "tourBodyClickAndDrag": {
     "message": "Nhấp và kéo để chụp một phần của một trang. Bạn cũng có thể di chuột để làm nổi bật lựa chọn của bạn."
   },
   "tourHeaderFullPage": {
-    "message": "Chụp Windows hoặc Toàn bộ trang"
+    "message": "Chụp cửa sổ hoặc toàn bộ trang"
   },
   "tourBodyFullPage": {
     "message": "Chọn các nút ở phía trên bên phải để chụp khu vực nhìn thấy được trong cửa sổ hoặc để chụp toàn bộ trang."
   },
   "tourSkip": {
-    "message": "SKIP"
+    "message": "BỎ QUA"
   },
   "tourNext": {
     "message": "Slide tiếp theo"
   },
   "tourPrevious": {
     "message": "Slide trước đó"
   },
   "tourDone": {
--- a/browser/extensions/screenshots/_locales/zh_CN/messages.json
+++ b/browser/extensions/screenshots/_locales/zh_CN/messages.json
@@ -136,17 +136,17 @@
   },
   "tourHeaderPageAction": {
     "message": "新的保存方法"
   },
   "tourBodyPageAction": {
     "message": "随时可以展开地址栏中的页面操作菜单来截图。"
   },
   "tourHeaderClickAndDrag": {
-    "message": "截取你所需"
+    "message": "任您截取"
   },
   "tourBodyClickAndDrag": {
     "message": "单击并拖动以截取页面某个区域。您也可以把光标移到你要的地方,高亮后单击即可截图。"
   },
   "tourHeaderFullPage": {
     "message": "截取窗口或整个页面"
   },
   "tourBodyFullPage": {
--- a/browser/extensions/screenshots/background/main.js
+++ b/browser/extensions/screenshots/background/main.js
@@ -5,26 +5,19 @@
 this.main = (function() {
   const exports = {};
 
   const pasteSymbol = (window.navigator.platform.match(/Mac/i)) ? "\u2318" : "Ctrl";
   const { sendEvent, incrementCount } = analytics;
 
   const manifest = browser.runtime.getManifest();
   let backend;
-  let _hasAnyShots = false;
-
-  startBackground.serverStatus.then((status) => {
-    _hasAnyShots = status.hasAny;
-  }).catch((e) => {
-    log.warn("Cannot see server status", e);
-  });
 
   exports.hasAnyShots = function() {
-    return _hasAnyShots;
+    return false;
   };
 
   let hasSeenOnboarding = browser.storage.local.get(["hasSeenOnboarding"]).then((result) => {
     const onboarded = !!result.hasSeenOnboarding;
     hasSeenOnboarding = Promise.resolve(onboarded);
     return hasSeenOnboarding;
   }).catch((error) => {
     log.error("Error getting hasSeenOnboarding:", error);
--- a/browser/extensions/screenshots/background/startBackground.js
+++ b/browser/extensions/screenshots/background/startBackground.js
@@ -5,26 +5,16 @@
      browser.runtime.onMessage
    and loads the rest of the background page in response to those events, forwarding
    the events to main.onClicked, main.onClickedContextMenu, or communication.onMessage
 */
 const startTime = Date.now();
 
 this.startBackground = (function() {
   const exports = {startTime};
-  // Wait until this many milliseconds to check the server for shots (for the purpose of migration warning):
-  const CHECK_SERVER_TIME = 5000; // 5 seconds
-  // If we want to pop open the tab showing the server status, wait this many milliseconds to open it:
-  const OPEN_SERVER_TAB_TIME = 5000; // 5 seconds
-  let hasSeenServerStatus = false;
-  let _resolveServerStatus;
-  exports.serverStatus = new Promise((resolve, reject) => {
-    _resolveServerStatus = {resolve, reject};
-  });
-  let backend;
 
   const backgroundScripts = [
     "log.js",
     "makeUuid.js",
     "catcher.js",
     "blobConverters.js",
     "background/selectorLoader.js",
     "background/communication.js",
@@ -109,89 +99,10 @@ this.startBackground = (function() {
           };
           document.head.appendChild(tag);
         });
       });
     });
     return loadedPromise;
   }
 
-  async function checkExpiration() {
-    const manifest = await browser.runtime.getManifest();
-    for (const permission of manifest.permissions) {
-      if (/^https?:\/\//.test(permission)) {
-        backend = permission.replace(/\/*$/, "");
-        break;
-      }
-    }
-    const result = await browser.storage.local.get(["registrationInfo", "hasSeenServerStatus", "hasShotsResponse"]);
-    hasSeenServerStatus = result.hasSeenServerStatus;
-    const { registrationInfo } = result;
-    if (!backend || !registrationInfo || !registrationInfo.registered) {
-      // The add-on hasn't been used, or at least no upload has occurred
-      _resolveServerStatus.resolve({hasIndefinite: false, hasAny: false});
-      return;
-    }
-    if (result.hasShotsResponse) {
-      // We've already retrieved information from the server
-      _resolveServerStatus.resolve(result.hasShotsResponse);
-      return;
-    }
-    const loginUrl = `${backend}/api/login`;
-    const hasShotsUrl = `${backend}/api/has-shots`;
-    try {
-      let resp = await fetch(loginUrl, {
-        method: "POST",
-        headers: {
-          "Content-Type": "application/json",
-        },
-        body: JSON.stringify({
-          deviceId: registrationInfo.deviceId,
-          secret: registrationInfo.secret,
-        }),
-      });
-      if (!resp.ok) {
-        throw new Error(`Bad login response: ${resp.status}`);
-      }
-      const { authHeader } = await resp.json();
-      resp = await fetch(hasShotsUrl, {
-        method: "GET",
-        credentials: "include",
-        headers: Object.assign({}, authHeader, {
-          "Content-Type": "application/json",
-        }),
-      });
-      if (!resp.ok) {
-        throw new Error(`Bad response from server: ${resp.status}`);
-      }
-      const body = await resp.json();
-      browser.storage.local.set({hasShotsResponse: body});
-      _resolveServerStatus.resolve(body);
-    } catch (e) {
-      _resolveServerStatus.reject(e);
-    }
-  }
-
-  exports.serverStatus.then((status) => {
-    if (status.hasAny) {
-      browser.experiments.screenshots.initLibraryButton();
-    }
-    if (status.hasIndefinite && !hasSeenServerStatus) {
-      setTimeout(async () => {
-        await browser.tabs.create({
-          url: `${backend}/hosting-shutdown`,
-        });
-        hasSeenServerStatus = true;
-        await browser.storage.local.set({hasSeenServerStatus});
-      }, OPEN_SERVER_TAB_TIME);
-    }
-  }).catch((e) => {
-    console.error("Error finding Screenshots server status:", String(e), e.stack);
-  });
-
-  setTimeout(() => {
-    window.requestIdleCallback(() => {
-      checkExpiration();
-    });
-  }, CHECK_SERVER_TIME);
-
   return exports;
 })();
--- a/browser/extensions/screenshots/manifest.json
+++ b/browser/extensions/screenshots/manifest.json
@@ -1,12 +1,12 @@
 {
   "manifest_version": 2,
   "name": "Firefox Screenshots",
-  "version": "37.1.0",
+  "version": "39.0.0",
   "description": "__MSG_addonDescription__",
   "author": "__MSG_addonAuthorsList__",
   "homepage_url": "https://github.com/mozilla-services/screenshots",
   "incognito": "spanning",
   "applications": {
     "gecko": {
       "id": "screenshots@mozilla.org",
       "strict_min_version": "57.0a1"
--- a/browser/extensions/screenshots/moz.build
+++ b/browser/extensions/screenshots/moz.build
@@ -92,20 +92,16 @@ FINAL_TARGET_FILES.features['screenshots
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["_locales"]["dsb"] += [
   '_locales/dsb/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["_locales"]["el"] += [
   '_locales/el/messages.json'
 ]
 
-FINAL_TARGET_FILES.features['screenshots@mozilla.org']["_locales"]["en_GB"] += [
-  '_locales/en_GB/messages.json'
-]
-
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["_locales"]["en_US"] += [
   '_locales/en_US/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["_locales"]["eo"] += [
   '_locales/eo/messages.json'
 ]
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -114,45 +114,45 @@
 @media (-moz-mac-yosemite-theme: 0) {
   .titlebar-spacer[type="fullscreen-button"] {
     margin-right: 4px;
   }
 }
 
 /** End titlebar **/
 
-#main-window[chromehidden~="toolbar"][chromehidden~="location"][chromehidden~="directories"] {
+:root[chromehidden~="toolbar"][chromehidden~="location"][chromehidden~="directories"] {
   border-top: 1px solid rgba(0,0,0,0.65);
 }
 
 .browser-toolbar:not(.titlebar-color) {
   -moz-appearance: none;
   background: var(--toolbar-bgcolor);
   color: var(--toolbar-color);
 }
 
 /* Draw the bottom border of the tabs toolbar when it's not using
    -moz-appearance: toolbar. */
-#main-window:-moz-any([sizemode="fullscreen"],[customize-entered]) #nav-bar:not([tabs-hidden="true"]),
-#main-window:not([tabsintitlebar]) #nav-bar:not([tabs-hidden="true"]),
+:root:-moz-any([sizemode="fullscreen"],[customize-entered]) #nav-bar:not([tabs-hidden="true"]),
+:root:not([tabsintitlebar]) #nav-bar:not([tabs-hidden="true"]),
 #nav-bar:not([tabs-hidden="true"]):-moz-lwtheme {
   box-shadow: 0 calc(-1 * var(--tabs-navbar-shadow-size)) 0 var(--tabs-border-color);
 }
 
 /* Always draw a border on Yosemite to ensure the border is well-defined there
  * (the default border is too light). */
 @media (-moz-mac-yosemite-theme) {
   #navigator-toolbox:not(:-moz-lwtheme) {
     --tabs-border-color: rgba(0,0,0,.2);
   }
   #navigator-toolbox:not(:-moz-lwtheme):-moz-window-inactive {
     --tabs-border-color: rgba(0,0,0,.05);
   }
 
-  #main-window[tabsintitlebar] #nav-bar:not([tabs-hidden="true"]):not(:-moz-lwtheme) {
+  :root[tabsintitlebar] #nav-bar:not([tabs-hidden="true"]):not(:-moz-lwtheme) {
     box-shadow: 0 calc(-1 * var(--tabs-navbar-shadow-size)) 0 var(--tabs-border-color);
   }
 }
 
 #nav-bar:not([tabs-hidden="true"]) {
   /* The toolbar buttons that animate are only visible when the #TabsToolbar is not collapsed.
      The animations use position:absolute and require a positioned #nav-bar. */
   position: relative;
@@ -223,17 +223,17 @@
    * their fill-opacity of 0.7. calc() doesn't work here - we'd need
    * to multiply two unitless numbers and that's invalid in CSS, so
    * we need to hard code the value for now. */
   fill-opacity: 0.28;
 }
 
 /* Inactive elements are faded out on OSX */
 .toolbarbutton-1:not(:hover):-moz-window-inactive,
-#main-window:not([customizing]) .toolbarbutton-1:-moz-window-inactive[disabled="true"] {
+:root:not([customizing]) .toolbarbutton-1:-moz-window-inactive[disabled="true"] {
   opacity: 0.5;
 }
 
 /* ----- FULLSCREEN WINDOW CONTROLS ----- */
 
 #minimize-button,
 #close-button,
 #fullscreen-button ~ #window-controls > #restore-button {
--- a/browser/themes/shared/aboutNetError.css
+++ b/browser/themes/shared/aboutNetError.css
@@ -70,37 +70,34 @@ button:disabled {
 body:not(.neterror) #certErrorAndCaptivePortalButtonContainer {
   display: flex;
 }
 
 body:not(.neterror) #netErrorButtonContainer {
   display: none;
 }
 
-#errorTryAgain {
+#netErrorButtonContainer > .try-again {
   margin-top: 1.2em;
 }
 
 #advancedButton {
   display: none;
 }
 
 body.captiveportal #returnButton {
   display: none;
 }
 
 body:not(.captiveportal) #openPortalLoginPageButton {
   display: none;
 }
 
-body:not(.clockSkewError) #errorTryAgain {
-  display: none;
-}
-
-body:not(.clockSkewError) #advancedPanelErrorTryAgain {
+body:not(.clockSkewError) #certErrorAndCaptivePortalButtonContainer > .try-again,
+body:not(.clockSkewError) #advancedPanelContainer .try-again {
   display: none;
 }
 
 #openPortalLoginPageButton {
   margin-inline-start: 0;
 }
 
 body:not(.neterror) #advancedButton {
@@ -228,31 +225,30 @@ body:not(.neterror) #advancedButton {
 .clockSkewError #returnButton {
   display: none;
 }
 
 .clockSkewError #advancedButton {
   display: none;
 }
 
-.clockSkewError #advancedPanelErrorTryAgain,
-.clockSkewError #errorTryAgain {
+.clockSkewError .try-again {
   display: block;
   margin-top: 0.3em;
 }
 
 .clockSkewError #exceptionDialogButton {
   display: none;
 }
 
 .clockSkewError #advancedPanelReturnButton {
   display: none;
 }
 
-.malformedURI #errorTryAgain {
+.malformedURI .try-again {
   display: none;
 }
 
 #wrongSystemTimePanel {
   display: none;
 }
 
 #wrongSystemTimeWithoutReferencePanel {
--- a/build/pgo/server-locations.txt
+++ b/build/pgo/server-locations.txt
@@ -293,10 +293,13 @@ https://ssl3rc4.example.com:443     priv
 https://tls1.example.com:443        privileged,tls1
 
 # Hosts for youtube rewrite tests
 https://mochitest.youtube.com:443
 
 # Host for U2F localhost tests
 https://localhost:443
 
+# Bug 1402530
+http://localhost:80                 privileged
+
 # Host for testing APIs whitelisted for mozilla.org
 https://www.mozilla.org:443
--- a/caps/nsIScriptSecurityManager.idl
+++ b/caps/nsIScriptSecurityManager.idl
@@ -21,17 +21,17 @@ class DomainPolicyClone;
 }
 }
 %}
 
 [ptr] native JSContextPtr(JSContext);
 [ptr] native JSObjectPtr(JSObject);
 [ptr] native DomainPolicyClonePtr(mozilla::dom::DomainPolicyClone);
 
-[scriptable, uuid(51daad87-3a0c-44cc-b620-7356801c9022)]
+[scriptable, builtinclass, uuid(51daad87-3a0c-44cc-b620-7356801c9022)]
 interface nsIScriptSecurityManager : nsISupports
 {
     /**
      * For each of these hooks returning NS_OK means 'let the action continue'.
      * Returning an error code means 'veto the action'. XPConnect will return
      * false to the js engine if the action is vetoed. The implementor of this
      * interface is responsible for setting a JS exception into the JSContext
      * if that is appropriate.
--- a/devtools/client/aboutdebugging-new/src/actions/runtimes.js
+++ b/devtools/client/aboutdebugging-new/src/actions/runtimes.js
@@ -81,40 +81,43 @@ function onRemoteDebuggerClientClosed() 
   window.AboutDebugging.onUSBRuntimesUpdated();
 }
 
 function onMultiE10sUpdated() {
   window.AboutDebugging.store.dispatch(updateMultiE10s());
 }
 
 function connectRuntime(id) {
+  // Create a random connection id to track the connection attempt in telemetry.
+  const connectionId = (Math.random() * 100000) | 0;
+
   return async (dispatch, getState) => {
-    dispatch({ type: CONNECT_RUNTIME_START, id });
+    dispatch({ type: CONNECT_RUNTIME_START, connectionId, id });
 
     // The preferences test-connection-timing-out-delay and test-connection-cancel-delay
     // don't have a default value but will be overridden during our tests.
     const connectionTimingOutDelay = Services.prefs.getIntPref(
       "devtools.aboutdebugging.test-connection-timing-out-delay",
       CONNECTION_TIMING_OUT_DELAY);
     const connectionCancelDelay = Services.prefs.getIntPref(
       "devtools.aboutdebugging.test-connection-cancel-delay", CONNECTION_CANCEL_DELAY);
 
     const connectionNotRespondingTimer = setTimeout(() => {
       // If connecting to the runtime takes time over CONNECTION_TIMING_OUT_DELAY,
       // we assume the connection prompt is showing on the runtime, show a dialog
       // to let user know that.
-      dispatch({ type: CONNECT_RUNTIME_NOT_RESPONDING, id });
+      dispatch({ type: CONNECT_RUNTIME_NOT_RESPONDING, connectionId, id });
     }, connectionTimingOutDelay);
     const connectionCancelTimer = setTimeout(() => {
       // Connect button of the runtime will be disabled during connection, but the status
       // continues till the connection was either succeed or failed. This may have a
       // possibility that the disabling continues unless page reloading, user will not be
       // able to click again. To avoid this, revert the connect button status after
       // CONNECTION_CANCEL_DELAY ms.
-      dispatch({ type: CONNECT_RUNTIME_CANCEL, id });
+      dispatch({ type: CONNECT_RUNTIME_CANCEL, connectionId, id });
     }, connectionCancelDelay);
 
     try {
       const runtime = findRuntimeById(id, getState().runtimes);
       const clientWrapper = await createClientForRuntime(runtime);
 
       const deviceDescription = await clientWrapper.getDeviceDescription();
       const compatibilityReport = await clientWrapper.checkVersionCompatibility();
@@ -178,24 +181,25 @@ function connectRuntime(id) {
       if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
         // `closed` event will be emitted when disabling remote debugging
         // on the connected remote runtime.
         clientWrapper.addOneTimeListener("closed", onRemoteDebuggerClientClosed);
       }
 
       dispatch({
         type: CONNECT_RUNTIME_SUCCESS,
+        connectionId,
         runtime: {
           id,
           runtimeDetails,
           type: runtime.type,
         },
       });
     } catch (e) {
-      dispatch({ type: CONNECT_RUNTIME_FAILURE, id, error: e });
+      dispatch({ type: CONNECT_RUNTIME_FAILURE, connectionId, id, error: e });
     } finally {
       clearTimeout(connectionNotRespondingTimer);
       clearTimeout(connectionCancelTimer);
     }
   };
 }
 
 function createThisFirefoxRuntime() {
--- a/devtools/client/aboutdebugging-new/src/middleware/event-recording.js
+++ b/devtools/client/aboutdebugging-new/src/middleware/event-recording.js
@@ -5,16 +5,20 @@
 "use strict";
 
 const Telemetry = require("devtools/client/shared/telemetry");
 loader.lazyGetter(this, "telemetry", () => new Telemetry());
 // This is a unique id that should be submitted with all about:debugging events.
 loader.lazyGetter(this, "sessionId", () => parseInt(telemetry.msSinceProcessStart(), 10));
 
 const {
+  CONNECT_RUNTIME_CANCEL,
+  CONNECT_RUNTIME_FAILURE,
+  CONNECT_RUNTIME_NOT_RESPONDING,
+  CONNECT_RUNTIME_START,
   CONNECT_RUNTIME_SUCCESS,
   DISCONNECT_RUNTIME_SUCCESS,
   REMOTE_RUNTIMES_UPDATED,
   RUNTIMES,
   SELECT_PAGE_SUCCESS,
   SHOW_PROFILER_DIALOG,
   TELEMETRY_RECORD,
   UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
@@ -141,23 +145,51 @@ function onRemoteRuntimesUpdated(action,
       recordEvent("device_added", {
         "connection_type": action.runtimeType,
         "device_name": newDeviceName,
       });
     }
   }
 }
 
+function recordConnectionAttempt(connectionId, runtimeId, status, store) {
+  const runtime = findRuntimeById(runtimeId, store.getState().runtimes);
+  if (runtime.type === RUNTIMES.THIS_FIREFOX) {
+    // Only record connection_attempt events for remote runtimes.
+    return;
+  }
+
+  recordEvent("connection_attempt", {
+    "connection_id": connectionId,
+    "connection_type": runtime.type,
+    "runtime_id": getTelemetryRuntimeId(runtimeId),
+    "status": status,
+  });
+}
+
 /**
  * This middleware will record events to telemetry for some specific actions.
  */
 function eventRecordingMiddleware(store) {
   return next => action => {
     switch (action.type) {
+      case CONNECT_RUNTIME_CANCEL:
+        recordConnectionAttempt(action.connectionId, action.id, "cancelled", store);
+        break;
+      case CONNECT_RUNTIME_FAILURE:
+        recordConnectionAttempt(action.connectionId, action.id, "failed", store);
+        break;
+      case CONNECT_RUNTIME_NOT_RESPONDING:
+        recordConnectionAttempt(action.connectionId, action.id, "not responding", store);
+        break;
+      case CONNECT_RUNTIME_START:
+        recordConnectionAttempt(action.connectionId, action.id, "start", store);
+        break;
       case CONNECT_RUNTIME_SUCCESS:
+        recordConnectionAttempt(action.connectionId, action.runtime.id, "success", store);
         onConnectRuntimeSuccess(action, store);
         break;
       case DISCONNECT_RUNTIME_SUCCESS:
         onDisconnectRuntimeSuccess(action, store);
         break;
       case REMOTE_RUNTIMES_UPDATED:
         onRemoteRuntimesUpdated(action, store);
         break;
--- a/devtools/client/aboutdebugging-new/test/browser/browser.ini
+++ b/devtools/client/aboutdebugging-new/test/browser/browser.ini
@@ -105,16 +105,17 @@ skip-if = debug || asan # Frequent inter
 [browser_aboutdebugging_sidebar_usb_runtime_refresh.js]
 [browser_aboutdebugging_sidebar_usb_runtime_select.js]
 [browser_aboutdebugging_sidebar_usb_status.js]
 [browser_aboutdebugging_sidebar_usb_unavailable_runtime.js]
 [browser_aboutdebugging_sidebar_usb_unplugged_device.js]
 [browser_aboutdebugging_hidden_addons.js]
 [browser_aboutdebugging_tab_favicons.js]
 [browser_aboutdebugging_telemetry_basic.js]
+[browser_aboutdebugging_telemetry_connection_attempt.js]
 [browser_aboutdebugging_telemetry_inspect.js]
 [browser_aboutdebugging_telemetry_navigate.js]
 [browser_aboutdebugging_telemetry_runtime_actions.js]
 [browser_aboutdebugging_telemetry_runtime_connected_details.js]
 [browser_aboutdebugging_telemetry_runtime_updates.js]
 [browser_aboutdebugging_telemetry_runtime_updates_multi.js]
 [browser_aboutdebugging_telemetry_runtime_updates_network.js]
 [browser_aboutdebugging_thisfirefox.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_telemetry_connection_attempt.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from helper-telemetry.js */
+Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-telemetry.js", this);
+
+const USB_RUNTIME = {
+  id: "runtime-id-1",
+  deviceName: "Device A",
+  name: "Runtime 1",
+  shortName: "R1",
+};
+
+/**
+ * Check that telemetry events for connection attempts are correctly recorded in various
+ * scenarios:
+ * - successful connection
+ * - successful connection after showing the timeout warning
+ * - failed connection
+ * - connection timeout
+ */
+add_task(async function testSuccessfulConnectionAttempt() {
+  const { doc, mocks, runtimeId, sessionId, tab } = await setupConnectionAttemptTest();
+
+  await connectToRuntime(USB_RUNTIME.deviceName, doc);
+
+  const connectionEvents = checkTelemetryEvents([
+    { method: "runtime_connected", extras: { runtime_id: runtimeId } },
+    { method: "connection_attempt", extras: getEventExtras("start", runtimeId) },
+    { method: "connection_attempt", extras: getEventExtras("success", runtimeId) },
+  ], sessionId).filter(({method}) => method === "connection_attempt");
+
+  checkConnectionId(connectionEvents);
+
+  await removeUsbRuntime(USB_RUNTIME, mocks, doc);
+  await removeTab(tab);
+});
+
+add_task(async function testFailedConnectionAttempt() {
+  const { doc, mocks, runtimeId, sessionId, tab } = await setupConnectionAttemptTest();
+  mocks.runtimeClientFactoryMock.createClientForRuntime = async (runtime) => {
+    throw new Error("failed");
+  };
+
+  info("Try to connect to the runtime and wait for the connection error message");
+  const usbRuntimeSidebarItem = findSidebarItemByText(USB_RUNTIME.deviceName, doc);
+  const connectButton = usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+  connectButton.click();
+  await waitUntil(() => usbRuntimeSidebarItem.querySelector(".qa-connection-error"));
+
+  const connectionEvents = checkTelemetryEvents([
+    { method: "connection_attempt", extras: getEventExtras("start", runtimeId) },
+    { method: "connection_attempt", extras: getEventExtras("failed", runtimeId) },
+  ], sessionId).filter(({method}) => method === "connection_attempt");
+
+  checkConnectionId(connectionEvents);
+
+  await removeUsbRuntime(USB_RUNTIME, mocks, doc);
+  await removeTab(tab);
+});
+
+add_task(async function testPendingConnectionAttempt() {
+  info("Set timeout preferences to avoid cancelling the connection");
+  await pushPref("devtools.aboutdebugging.test-connection-timing-out-delay", 100);
+  await pushPref("devtools.aboutdebugging.test-connection-cancel-delay", 100000);
+
+  const { doc, mocks, runtimeId, sessionId, tab } = await setupConnectionAttemptTest();
+
+  info("Simulate a pending connection");
+  let resumeConnection;
+  const resumeConnectionPromise = new Promise(r => {
+    resumeConnection = r;
+  });
+  mocks.runtimeClientFactoryMock.createClientForRuntime = async (runtime) => {
+    await resumeConnectionPromise;
+    return mocks._clients[runtime.type][runtime.id];
+  };
+
+  info("Click on the connect button and wait for the warning message");
+  const usbRuntimeSidebarItem = findSidebarItemByText(USB_RUNTIME.deviceName, doc);
+  const connectButton = usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+  connectButton.click();
+  await waitUntil(() => doc.querySelector(".qa-connection-not-responding"));
+
+  info("Resume the connection and wait for the connection to succeed");
+  resumeConnection();
+  await waitUntil(() => !usbRuntimeSidebarItem.querySelector(".qa-connect-button"));
+
+  const connectionEvents = checkTelemetryEvents([
+    { method: "runtime_connected", extras: { runtime_id: runtimeId } },
+    { method: "connection_attempt", extras: getEventExtras("start", runtimeId) },
+    { method: "connection_attempt", extras: getEventExtras("not responding", runtimeId) },
+    { method: "connection_attempt", extras: getEventExtras("success", runtimeId) },
+  ], sessionId).filter(({method}) => method === "connection_attempt");
+  checkConnectionId(connectionEvents);
+
+  await removeUsbRuntime(USB_RUNTIME, mocks, doc);
+  await removeTab(tab);
+});
+
+add_task(async function testCancelledConnectionAttempt() {
+  info("Set timeout preferences to quickly cancel the connection");
+  await pushPref("devtools.aboutdebugging.test-connection-timing-out-delay", 100);
+  await pushPref("devtools.aboutdebugging.test-connection-cancel-delay", 1000);
+
+  const { doc, mocks, runtimeId, sessionId, tab } = await setupConnectionAttemptTest();
+
+  info("Simulate a connection timeout");
+  mocks.runtimeClientFactoryMock.createClientForRuntime = async (runtime) => {
+    await new Promise(r => {});
+  };
+
+  info("Click on the connect button and wait for the error message");
+  const usbRuntimeSidebarItem = findSidebarItemByText(USB_RUNTIME.deviceName, doc);
+  const connectButton = usbRuntimeSidebarItem.querySelector(".qa-connect-button");
+  connectButton.click();
+  await waitUntil(() => usbRuntimeSidebarItem.querySelector(".qa-connection-timeout"));
+
+  const connectionEvents = checkTelemetryEvents([
+    { method: "connection_attempt", extras: getEventExtras("start", runtimeId) },
+    { method: "connection_attempt", extras: getEventExtras("not responding", runtimeId) },
+    { method: "connection_attempt", extras: getEventExtras("cancelled", runtimeId) },
+  ], sessionId).filter(({method}) => method === "connection_attempt");
+  checkConnectionId(connectionEvents);
+
+  await removeUsbRuntime(USB_RUNTIME, mocks, doc);
+  await removeTab(tab);
+});
+
+function checkConnectionId(connectionEvents) {
+  const connectionId = connectionEvents[0].extras.connection_id;
+  ok(!!connectionId, "Found a valid connection id in the first connection_attempt event");
+  for (const evt of connectionEvents) {
+    is(evt.extras.connection_id, connectionId,
+      "All connection_attempt events share the same connection id");
+  }
+}
+
+// Small helper to create the expected event extras object for connection_attempt events
+function getEventExtras(status, runtimeId) {
+  return {
+    connection_type: "usb",
+    runtime_id: runtimeId,
+    status,
+  };
+}
+
+// Open about:debugging, setup telemetry, mocks and create a mocked USB runtime.
+async function setupConnectionAttemptTest() {
+  const mocks = new Mocks();
+  setupTelemetryTest();
+
+  const { tab, document } = await openAboutDebugging();
+
+  const sessionId = getOpenEventSessionId();
+  ok(!isNaN(sessionId), "Open event has a valid session id");
+
+  mocks.createUSBRuntime(USB_RUNTIME.id, {
+    deviceName: USB_RUNTIME.deviceName,
+    name: USB_RUNTIME.name,
+    shortName: USB_RUNTIME.shortName,
+  });
+  mocks.emitUSBUpdate();
+
+  info("Wait for the runtime to appear in the sidebar");
+  await waitUntil(() => findSidebarItemByText(USB_RUNTIME.shortName, document));
+  const evts = checkTelemetryEvents([
+    { method: "device_added", extras: {} },
+    { method: "runtime_added", extras: {} },
+  ], sessionId);
+
+  const runtimeId = evts.filter(e => e.method === "runtime_added")[0].extras.runtime_id;
+  return { doc: document, mocks, runtimeId, sessionId, tab };
+}
+
+async function removeUsbRuntime(runtime, mocks, doc) {
+  mocks.removeRuntime(runtime.id);
+  mocks.emitUSBUpdate();
+  await waitUntil(() => !findSidebarItemByText(runtime.name, doc) &&
+                        !findSidebarItemByText(runtime.shortName, doc));
+}
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_telemetry_runtime_updates.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_telemetry_runtime_updates.js
@@ -72,16 +72,18 @@ add_task(async function testUsbRuntimeUp
   const runtime1ConnectedExtras = Object.assign({}, runtime1Extras, {
     "runtime_name": USB_RUNTIME_1.name,
   });
 
   await connectToRuntime(USB_RUNTIME_1.deviceName, document);
 
   checkTelemetryEvents([
     { method: "runtime_connected", extras: runtime1ConnectedExtras },
+    { method: "connection_attempt", extras: { status: "start" } },
+    { method: "connection_attempt", extras: { status: "success" } },
   ], sessionId);
 
   info("Add a second runtime");
   await addUsbRuntime(USB_RUNTIME_2, mocks, document);
   evts = checkTelemetryEvents([
     { method: "runtime_added", extras: RUNTIME_2_EXTRAS },
   ], sessionId);
 
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_telemetry_runtime_updates_network.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_telemetry_runtime_updates_network.js
@@ -45,16 +45,18 @@ add_task(async function testNetworkRunti
   // device_added event.
   checkTelemetryEvents([
     { method: "runtime_added", extras: networkRuntimeExtras },
   ], sessionId);
 
   await connectToRuntime(NETWORK_RUNTIME.host, document);
   checkTelemetryEvents([
     { method: "runtime_connected", extras: connectedNetworkRuntimeExtras },
+    { method: "connection_attempt", extras: { status: "start" } },
+    { method: "connection_attempt", extras: { status: "success" } },
   ], sessionId);
 
   info("Remove network runtime");
   mocks.removeRuntime(NETWORK_RUNTIME.host);
   await waitUntil(() => !findSidebarItemByText(NETWORK_RUNTIME.host, document));
   // Similarly we should not have any device removed event.
   checkTelemetryEvents([
     { method: "runtime_disconnected", extras: connectedNetworkRuntimeExtras },
--- a/devtools/client/accessibility/accessibility.css
+++ b/devtools/client/accessibility/accessibility.css
@@ -171,16 +171,35 @@ body {
   display: flex;
   flex-wrap: nowrap;
   flex-direction: row;
   align-items: center;
   white-space: nowrap;
   margin-inline-end: 5px;
 }
 
+#audit-progress-container {
+  position: fixed;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  z-index: 9999;
+  background: rgba(255,255,255,0.9);
+  padding-block-start: 30vh;
+  font: message-box;
+  font-size: 12px;
+  font-style: italic;
+}
+
+.audit-progress-progressbar {
+  width: 30vw;
+}
+
 /* Description */
 .description {
   color: var(--theme-toolbar-color);
   font: message-box;
   font-size: calc(var(--accessibility-font-size) + 1px);
   margin: auto;
   padding-top: 15vh;
   width: 50vw;
--- a/devtools/client/accessibility/actions/audit.js
+++ b/devtools/client/accessibility/actions/audit.js
@@ -1,23 +1,39 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { AUDIT, AUDITING, FILTER_TOGGLE } = require("../constants");
+const { AUDIT, AUDIT_PROGRESS, AUDITING, FILTER_TOGGLE } = require("../constants");
 
 exports.filterToggle = filter =>
   dispatch => dispatch({ filter, type: FILTER_TOGGLE });
 
 exports.auditing = filter =>
   dispatch => dispatch({ auditing: filter, type: AUDITING });
 
 exports.audit = (walker, filter) =>
-  dispatch => {
-    const onAuditEvent = walker.once("audit-event");
+  dispatch => new Promise(resolve => {
+    const auditEventHandler = ({ type, ancestries, progress }) => {
+      switch (type) {
+        case "error":
+          walker.off("audit-event", auditEventHandler);
+          dispatch({ type: AUDIT, error: true });
+          resolve();
+          break;
+        case "completed":
+          walker.off("audit-event", auditEventHandler);
+          dispatch({ type: AUDIT, response: ancestries });
+          resolve();
+          break;
+        case "progress":
+          dispatch({ type: AUDIT_PROGRESS, progress });
+          break;
+        default:
+          break;
+      }
+    };
+
+    walker.on("audit-event", auditEventHandler);
     walker.startAudit();
-    return onAuditEvent
-      .then(({ ancestries: response, error }) =>
-        dispatch({ type: AUDIT, error, response }))
-      .catch(error => dispatch({ type: AUDIT, error }));
-  };
+  });
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/components/AuditProgressOverlay.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { PluralForm } = require("devtools/shared/plural-form");
+
+const { L10N } = require("../utils/l10n");
+
+/**
+ * Helper functional component to render an accessible text progressbar.
+ * @param {Object} props
+ *        - id for the progressbar element
+ *        - valuetext for the progressbar element
+ */
+function TextProgressBar({ id, textStringKey }) {
+  const text = L10N.getStr(textStringKey);
+  return ReactDOM.span({
+    id,
+    key: id,
+    role: "progressbar",
+    "aria-valuetext": text,
+  },
+    text);
+}
+
+class AuditProgressOverlay extends React.Component {
+  static get propTypes() {
+    return {
+      auditing: PropTypes.array,
+      total: PropTypes.number,
+      percentage: PropTypes.number,
+    };
+  }
+
+  render() {
+    const { auditing, percentage, total } = this.props;
+    if (!auditing) {
+      return null;
+    }
+
+    const id = "audit-progress-container";
+
+    if (total == null) {
+      return TextProgressBar({id, textStringKey: "accessibility.progress.initializing"});
+    }
+
+    if (percentage === 100) {
+      return TextProgressBar({id, textStringKey: "accessibility.progress.finishing"});
+    }
+
+    const progressbarString = PluralForm.get(total,
+      L10N.getStr("accessibility.progress.progressbar"));
+
+    return ReactDOM.span({
+      id,
+      key: id,
+    },
+      progressbarString.replace("#1", total),
+      ReactDOM.progress({
+        max: 100,
+        value: percentage,
+        className: "audit-progress-progressbar",
+        "aria-labelledby": id,
+      }));
+  }
+}
+
+const mapStateToProps = ({ audit: { auditing, progress }}) => {
+  const { total, percentage } = progress || {};
+  return { auditing, total, percentage };
+};
+
+module.exports = connect(mapStateToProps)(AuditProgressOverlay);
--- a/devtools/client/accessibility/components/MainFrame.js
+++ b/devtools/client/accessibility/components/MainFrame.js
@@ -2,42 +2,44 @@
  * 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";
 
 /* global gToolbox */
 
 // React & Redux
 const { Component, createFactory } = require("devtools/client/shared/vendor/react");
-const { div } = require("devtools/client/shared/vendor/react-dom-factories");
+const { span, div } = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { reset } = require("../actions/ui");
 
 // Constants
 const { SIDEBAR_WIDTH, PORTRAIT_MODE_WIDTH } = require("../constants");
 
 // Accessibility Panel
 const AccessibilityTree = createFactory(require("./AccessibilityTree"));
+const AuditProgressOverlay = createFactory(require("./AuditProgressOverlay"));
 const Description = createFactory(require("./Description").Description);
 const RightSidebar = createFactory(require("./RightSidebar"));
 const Toolbar = createFactory(require("./Toolbar"));
 const SplitBox = createFactory(require("devtools/client/shared/components/splitter/SplitBox"));
 
 /**
  * Renders basic layout of the Accessibility panel. The Accessibility panel
  * content consists of two main parts: tree and sidebar.
  */
 class MainFrame extends Component {
   static get propTypes() {
     return {
       accessibility: PropTypes.object.isRequired,
       walker: PropTypes.object.isRequired,
       enabled: PropTypes.bool.isRequired,
       dispatch: PropTypes.func.isRequired,
+      auditing: PropTypes.string,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.resetAccessibility = this.resetAccessibility.bind(this);
     this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
@@ -84,41 +86,48 @@ class MainFrame extends Component {
       this.refs.splitBox.setState({ vert: this.useLandscapeMode });
     }
   }
 
   /**
    * Render Accessibility panel content
    */
   render() {
-    const { accessibility, walker, enabled } = this.props;
+    const { accessibility, walker, enabled, auditing } = this.props;
 
     if (!enabled) {
       return Description({ accessibility });
     }
 
     return (
       div({ className: "mainFrame", role: "presentation" },
         Toolbar({ accessibility, walker }),
-        SplitBox({
-          ref: "splitBox",
-          initialSize: SIDEBAR_WIDTH,
-          minSize: "10px",
-          maxSize: "80%",
-          splitterSize: 1,
-          endPanelControl: true,
-          startPanel: div({
-            className: "main-panel",
-            role: "presentation",
-          }, AccessibilityTree({ walker })),
-          endPanel: RightSidebar({ walker }),
-          vert: this.useLandscapeMode,
-        })
+        auditing && AuditProgressOverlay(),
+        span({
+          "aria-hidden": !!auditing,
+          role: "presentation",
+          style: { display: "contents" },
+        },
+          SplitBox({
+            ref: "splitBox",
+            initialSize: SIDEBAR_WIDTH,
+            minSize: "10px",
+            maxSize: "80%",
+            splitterSize: 1,
+            endPanelControl: true,
+            startPanel: div({
+              className: "main-panel",
+              role: "presentation",
+            }, AccessibilityTree({ walker })),
+            endPanel: RightSidebar({ walker }),
+            vert: this.useLandscapeMode,
+          })),
       ));
   }
 }
 
-const mapStateToProps = ({ ui }) => ({
+const mapStateToProps = ({ ui, audit: { auditing } }) => ({
   enabled: ui.enabled,
+  auditing,
 });
 
 // Exports from this module
 module.exports = connect(mapStateToProps)(MainFrame);
--- a/devtools/client/accessibility/components/moz.build
+++ b/devtools/client/accessibility/components/moz.build
@@ -5,16 +5,17 @@
 DevToolsModules(
     'AccessibilityRow.js',
     'AccessibilityRowValue.js',
     'AccessibilityTree.js',
     'AccessibilityTreeFilter.js',
     'Accessible.js',
     'AuditController.js',
     'AuditFilter.js',
+    'AuditProgressOverlay.js',
     'Badge.js',
     'Badges.js',
     'Button.js',
     'Checks.js',
     'ColorContrastAccessibility.js',
     'ContrastBadge.js',
     'Description.js',
     'LearnMoreLink.js',
--- a/devtools/client/accessibility/constants.js
+++ b/devtools/client/accessibility/constants.js
@@ -29,16 +29,17 @@ exports.HIGHLIGHT = "HIGHLIGHT";
 exports.UNHIGHLIGHT = "UNHIGHLIGHT";
 exports.ENABLE = "ENABLE";
 exports.DISABLE = "DISABLE";
 exports.UPDATE_CAN_BE_DISABLED = "UPDATE_CAN_BE_DISABLED";
 exports.UPDATE_CAN_BE_ENABLED = "UPDATE_CAN_BE_ENABLED";
 exports.FILTER_TOGGLE = "FILTER_TOGGLE";
 exports.AUDIT = "AUDIT";
 exports.AUDITING = "AUDITING";
+exports.AUDIT_PROGRESS = "AUDIT_PROGRESS";
 
 // List of filters for accessibility checks.
 exports.FILTERS = {
   [AUDIT_TYPE.CONTRAST]: "CONTRAST",
 };
 
 // Ordered accessible properties to be displayed by the accessible component.
 exports.ORDERED_PROPS = [
--- a/devtools/client/accessibility/reducers/audit.js
+++ b/devtools/client/accessibility/reducers/audit.js
@@ -1,31 +1,33 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {
   AUDIT,
   AUDITING,
+  AUDIT_PROGRESS,
   FILTER_TOGGLE,
   FILTERS,
   RESET,
   SELECT,
 } = require("../constants");
 
 /**
  * Initial state definition
  */
 function getInitialState() {
   return {
     filters: {
       [FILTERS.CONTRAST]: false,
     },
     auditing: null,
+    progress: null,
   };
 }
 
 function audit(state = getInitialState(), action) {
   switch (action.type) {
     case FILTER_TOGGLE:
       const { filter } = action;
       let { filters } = state;
@@ -45,16 +47,24 @@ function audit(state = getInitialState()
       return {
         ...state,
         auditing,
       };
     case AUDIT:
       return {
         ...state,
         auditing: null,
+        progress: null,
+      };
+    case AUDIT_PROGRESS:
+      const { progress } = action;
+
+      return {
+        ...state,
+        progress,
       };
     case SELECT:
     case RESET:
       return getInitialState();
     default:
       return state;
   }
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/jest/components/__snapshots__/audit-progress-overlay.test.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AuditProgressOverlay component: render auditing finishing up 1`] = `"<span id=\\"audit-progress-container\\" role=\\"progressbar\\" aria-valuetext=\\"accessibility.progress.finishing\\">accessibility.progress.finishing</span>"`;
+
+exports[`AuditProgressOverlay component: render auditing initializing 1`] = `"<span id=\\"audit-progress-container\\" role=\\"progressbar\\" aria-valuetext=\\"accessibility.progress.initializing\\">accessibility.progress.initializing</span>"`;
+
+exports[`AuditProgressOverlay component: render auditing progress 1`] = `"<span id=\\"audit-progress-container\\">accessibility.progress.progressbar<progress max=\\"100\\" value=\\"0\\" class=\\"audit-progress-progressbar\\" aria-labelledby=\\"audit-progress-container\\"></progress></span>"`;
+
+exports[`AuditProgressOverlay component: render auditing progress 2`] = `"<span id=\\"audit-progress-container\\">accessibility.progress.progressbar<progress max=\\"100\\" value=\\"50\\" class=\\"audit-progress-progressbar\\" aria-labelledby=\\"audit-progress-container\\"></progress></span>"`;
+
+exports[`AuditProgressOverlay component: render auditing progress 3`] = `"<span id=\\"audit-progress-container\\">accessibility.progress.progressbar<progress max=\\"100\\" value=\\"75\\" class=\\"audit-progress-progressbar\\" aria-labelledby=\\"audit-progress-container\\"></progress></span>"`;
+
+exports[`AuditProgressOverlay component: render not auditing 1`] = `null`;
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/jest/components/audit-progress-overlay.test.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { mount } = require("enzyme");
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const { setupStore } = require("devtools/client/accessibility/test/jest/helpers");
+
+const { accessibility: { AUDIT_TYPE } } = require("devtools/shared/constants");
+const { AUDIT_PROGRESS } = require("devtools/client/accessibility/constants");
+
+const ConnectedAuditProgressOverlayClass =
+  require("devtools/client/accessibility/components/AuditProgressOverlay");
+const AuditProgressOverlayClass = ConnectedAuditProgressOverlayClass.WrappedComponent;
+const AuditProgressOverlay = createFactory(ConnectedAuditProgressOverlayClass);
+
+function testTextProgressBar(store, expectedText) {
+  const wrapper = mount(Provider({ store }, AuditProgressOverlay()));
+  expect(wrapper.html()).toMatchSnapshot();
+
+  const overlay = wrapper.find(AuditProgressOverlayClass);
+  expect(overlay.children().length).toBe(1);
+
+  const overlayText = overlay.childAt(0);
+  expect(overlayText.type()).toBe("span");
+  expect(overlayText.prop("id")).toBe("audit-progress-container");
+  expect(overlayText.prop("role")).toBe("progressbar");
+  expect(overlayText.prop("aria-valuetext")).toBe(expectedText);
+  expect(overlayText.text()).toBe(expectedText);
+}
+
+function testProgress(wrapper, percentage) {
+  const progress = wrapper.find("progress");
+  expect(progress.prop("max")).toBe(100);
+  expect(progress.prop("value")).toBe(percentage);
+  expect(progress.hasClass("audit-progress-progressbar")).toBe(true);
+  expect(progress.prop("aria-labelledby")).toBe("audit-progress-container");
+}
+
+describe("AuditProgressOverlay component:", () => {
+  it("render not auditing", () => {
+    const store = setupStore();
+    const wrapper = mount(Provider({ store }, AuditProgressOverlay()));
+    expect(wrapper.html()).toMatchSnapshot();
+    expect(wrapper.isEmptyRender()).toBe(true);
+  });
+
+  it("render auditing initializing", () => {
+    const store = setupStore({
+      preloadedState: { audit: { auditing: AUDIT_TYPE.CONTRAST } },
+    });
+
+    testTextProgressBar(store, "accessibility.progress.initializing");
+  });
+
+  it("render auditing progress", () => {
+    const store = setupStore({
+      preloadedState: {
+        audit: {
+          auditing: AUDIT_TYPE.CONTRAST,
+          progress: { total: 5, percentage: 0 },
+        },
+      },
+    });
+
+    const wrapper = mount(Provider({ store }, AuditProgressOverlay()));
+    expect(wrapper.html()).toMatchSnapshot();
+
+    const overlay = wrapper.find(AuditProgressOverlayClass);
+    expect(overlay.children().length).toBe(1);
+
+    const overlayContainer = overlay.childAt(0);
+    expect(overlayContainer.type()).toBe("span");
+    expect(overlayContainer.prop("id")).toBe("audit-progress-container");
+    expect(overlayContainer.children().length).toBe(1);
+
+    expect(overlayContainer.text()).toBe("accessibility.progress.progressbar");
+    expect(overlayContainer.childAt(0).type()).toBe("progress");
+
+    testProgress(wrapper, 0);
+
+    store.dispatch({
+      type: AUDIT_PROGRESS,
+      progress: { total: 5, percentage: 50 },
+    });
+    wrapper.update();
+
+    expect(wrapper.html()).toMatchSnapshot();
+    testProgress(wrapper, 50);
+
+    store.dispatch({
+      type: AUDIT_PROGRESS,
+      progress: { total: 5, percentage: 75 },
+    });
+    wrapper.update();
+
+    expect(wrapper.html()).toMatchSnapshot();
+    testProgress(wrapper, 75);
+  });
+
+  it("render auditing finishing up", () => {
+    const store = setupStore({
+      preloadedState: {
+        audit: {
+          auditing: AUDIT_TYPE.CONTRAST,
+          progress: { total: 5, percentage: 100 },
+        },
+      },
+    });
+
+    testTextProgressBar(store, "accessibility.progress.finishing");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/jest/fixtures/plural-form.js
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+module.exports.PluralForm = {
+  get(num, str) {
+    return str;
+  },
+};
--- a/devtools/client/accessibility/test/jest/jest.config.js
+++ b/devtools/client/accessibility/test/jest/jest.config.js
@@ -7,16 +7,17 @@
 /* global __dirname */
 
 module.exports = {
   verbose: true,
   moduleNameMapper: {
     // Custom name mappers for modules that require m-c specific API.
     "^../utils/l10n": `${__dirname}/fixtures/l10n`,
     "^devtools/client/shared/link": `${__dirname}/fixtures/stub`,
+    "^devtools/shared/plural-form": `${__dirname}/fixtures/plural-form`,
     "^devtools/client/shared/components/tree/TreeView": `${__dirname}/fixtures/stub`,
     "^Services": `${__dirname}/fixtures/Services`,
     // Map all require("devtools/...") to the real devtools root.
     "^devtools\\/(.*)": `${__dirname}/../../../../$1`,
   },
   setupFiles: [
     "<rootDir>setup.js",
   ],
--- a/devtools/client/debugger/packages/devtools-reps/src/reps/rep-utils.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/reps/rep-utils.js
@@ -7,17 +7,17 @@ const validProtocols = /(http|https|ftp|
 
 // URL Regex, common idioms:
 //
 // Lead-in (URL):
 // (                     Capture because we need to know if there was a lead-in
 //                       character so we can include it as part of the text
 //                       preceding the match. We lack look-behind matching.
 //  ^|                   The URL can start at the beginning of the string.
-//  [\s(,;'"`]           Or whitespace or some punctuation that does not imply
+//  [\s(,;'"`“]          Or whitespace or some punctuation that does not imply
 //                       a context which would preclude a URL.
 // )
 //
 // We do not need a trailing look-ahead because our regex's will terminate
 // because they run out of characters they can eat.
 
 // What we do not attempt to have the regexp do:
 // - Avoid trailing '.' and ')' characters.  We let our greedy match absorb
@@ -48,17 +48,17 @@ const validProtocols = /(http|https|ftp|
 //                       found here: http://www.iana.org/domains/root/db
 //  )
 //  [-\w.!~*'();,/?:@&=+$#%]*
 //                       path onwards. We allow the set of characters that
 //                       encodeURI does not escape plus the result of escaping
 //                       (so also '%')
 // )
 // eslint-disable-next-line max-len
-const urlRegex = /(^|[\s(,;'"`])((?:https?:\/\/|www\d{0,3}[.][a-z0-9.\-]{2,249}|[a-z0-9.\-]{2,250}[.][a-z]{2,4}\/)[-\w.!~*'();,/?:@&=+$#%]*)/im;
+const urlRegex = /(^|[\s(,;'"`“])((?:https?:\/\/|www\d{0,3}[.][a-z0-9.\-]{2,249}|[a-z0-9.\-]{2,250}[.][a-z]{2,4}\/)[-\w.!~*'();,/?:@&=+$#%]*)/im;
 
 // Set of terminators that are likely to have been part of the context rather
 // than part of the URL and so should be uneaten. This is '(', ',', ';', plus
 // quotes and question end-ing punctuation and the potential permutations with
 // parentheses (english-specific).
 const uneatLastUrlCharsRegex = /(?:[),;.!?`'"]|[.!?]\)|\)[.!?])$/;
 
 const ELLIPSIS = "\u2026";
--- a/devtools/client/debugger/packages/devtools-reps/src/reps/string.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/reps/string.js
@@ -24,29 +24,31 @@ const { a, span } = dom;
 /**
  * Renders a string. String value is enclosed within quotes.
  */
 StringRep.propTypes = {
   useQuotes: PropTypes.bool,
   escapeWhitespace: PropTypes.bool,
   style: PropTypes.object,
   cropLimit: PropTypes.number.isRequired,
+  urlCropLimit: PropTypes.number,
   member: PropTypes.object,
   object: PropTypes.object.isRequired,
   openLink: PropTypes.func,
   className: PropTypes.string,
   title: PropTypes.string,
   isInContentPage: PropTypes.bool
 };
 
 function StringRep(props) {
   const {
     className,
     style,
     cropLimit,
+    urlCropLimit,
     object,
     useQuotes = true,
     escapeWhitespace = true,
     member,
     openLink,
     title,
     isInContentPage
   } = props;
@@ -86,22 +88,23 @@ function StringRep(props) {
     actor: object.actor,
     title
   });
 
   if (!isLong) {
     if (containsURL(text)) {
       return span(
         config,
-        ...getLinkifiedElements(
+        getLinkifiedElements({
           text,
-          shouldCrop && cropLimit,
+          cropLimit: shouldCrop ? cropLimit : null,
+          urlCropLimit,
           openLink,
           isInContentPage
-        )
+        })
       );
     }
 
     // Cropping of longString has been handled before formatting.
     text = maybeCropString(
       {
         isLong,
         shouldCrop,
@@ -167,24 +170,34 @@ function maybeCropString(opts, text) {
 
   return shouldCrop ? rawCropString(text, cropLimit) : text;
 }
 
 /**
  * Get an array of the elements representing the string, cropped if needed,
  * with actual links.
  *
- * @param {String} text: The actual string to linkify.
- * @param {Integer | null} cropLimit
- * @param {Function} openLink: Function handling the link opening.
- * @param {Boolean} isInContentPage: pass true if the reps is rendered in
- *                                   the content page (e.g. in JSONViewer).
+ * @param {Object} An options object of the following shape:
+ *                 - text {String}: The actual string to linkify.
+ *                 - cropLimit {Integer}: The limit to apply on the whole text.
+ *                 - urlCropLimit {Integer}: The limit to apply on each URL.
+ *                 - openLink {Function} openLink: Function handling the link
+ *                                                 opening.
+ *                 - isInContentPage {Boolean}: pass true if the reps is
+ *                                              rendered in the content page
+ *                                              (e.g. in JSONViewer).
  * @returns {Array<String|ReactElement>}
  */
-function getLinkifiedElements(text, cropLimit, openLink, isInContentPage) {
+function getLinkifiedElements({
+  text,
+  cropLimit,
+  urlCropLimit,
+  openLink,
+  isInContentPage
+}) {
   const halfLimit = Math.ceil((cropLimit - ELLIPSIS.length) / 2);
   const startCropIndex = cropLimit ? halfLimit : null;
   const endCropIndex = cropLimit ? text.length - halfLimit : null;
 
   const items = [];
   let currentIndex = 0;
   let contentStart;
   while (true) {
@@ -206,27 +219,38 @@ function getLinkifiedElements(text, crop
     // URL.
     let useUrl = url[2];
     const uneat = uneatLastUrlCharsRegex.exec(useUrl);
     if (uneat) {
       useUrl = useUrl.substring(0, uneat.index);
     }
 
     currentIndex = currentIndex + contentStart;
-    const linkText = getCroppedString(
+    let linkText = getCroppedString(
       useUrl,
       currentIndex,
       startCropIndex,
       endCropIndex
     );
 
     if (linkText) {
+      if (urlCropLimit && useUrl.length > urlCropLimit) {
+        const urlCropHalf = Math.ceil((urlCropLimit - ELLIPSIS.length) / 2);
+        linkText = getCroppedString(
+          useUrl,
+          0,
+          urlCropHalf,
+          useUrl.length - urlCropHalf
+        );
+      }
+
       items.push(
         a(
           {
+            key: `${useUrl}-${currentIndex}`,
             className: "url",
             title: useUrl,
             draggable: false,
             // Because we don't want the link to be open in the current
             // panel's frame, we only render the href attribute if `openLink`
             // exists (so we can preventDefault) or if the reps will be
             // displayed in content page (e.g. in the JSONViewer).
             href: openLink || isInContentPage ? useUrl : null,
--- a/devtools/client/debugger/packages/devtools-reps/src/reps/tests/string-with-url.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/reps/tests/string-with-url.js
@@ -381,37 +381,111 @@ describe("test String with URL", () => {
     const linkFr = element.find("a").at(0);
     expect(linkFr.prop("href")).toBe("http://example.fr");
     expect(linkFr.prop("title")).toBe("http://example.fr");
   });
 
   it("renders URLs without unrelated characters", () => {
     const text =
       "global(http://example.com) and local(http://example.us)" +
-      " and maybe https://example.fr, https://example.es?";
+      " and maybe https://example.fr, “https://example.cz“, https://example.es?";
     const openLink = jest.fn();
     const element = renderRep(text, {
       openLink,
       useQuotes: false
     });
 
     expect(element.text()).toEqual(text);
     const linkCom = element.find("a").at(0);
     expect(linkCom.prop("href")).toBe("http://example.com");
 
     const linkUs = element.find("a").at(1);
     expect(linkUs.prop("href")).toBe("http://example.us");
 
     const linkFr = element.find("a").at(2);
     expect(linkFr.prop("href")).toBe("https://example.fr");
 
-    const linkEs = element.find("a").at(3);
+    const linkCz = element.find("a").at(3);
+    expect(linkCz.prop("href")).toBe("https://example.cz");
+
+    const linkEs = element.find("a").at(4);
     expect(linkEs.prop("href")).toBe("https://example.es");
   });
 
+  it("renders a cropped URL with urlCropLimit", () => {
+    const xyzUrl = "http://xyz.com/abcdefghijklmnopqrst";
+    const text = `${xyzUrl} is the best`;
+    const openLink = jest.fn();
+    const element = renderRep(text, {
+      openLink,
+      useQuotes: false,
+      urlCropLimit: 20
+    });
+
+    expect(element.text()).toEqual("http://xyz…klmnopqrst is the best");
+    const link = element.find("a").at(0);
+    expect(link.prop("href")).toBe(xyzUrl);
+    expect(link.prop("title")).toBe(xyzUrl);
+  });
+
+  it("renders multiple cropped URL", () => {
+    const xyzUrl = "http://xyz.com/abcdefghijklmnopqrst";
+    const abcUrl = "http://abc.com/abcdefghijklmnopqrst";
+    const text = `${xyzUrl} is lit, not ${abcUrl}`;
+    const openLink = jest.fn();
+    const element = renderRep(text, {
+      openLink,
+      useQuotes: false,
+      urlCropLimit: 20
+    });
+
+    expect(element.text()).toEqual(
+      "http://xyz…klmnopqrst is lit, not http://abc…klmnopqrst"
+    );
+
+    const links = element.find("a");
+    const xyzLink = links.at(0);
+    expect(xyzLink.prop("href")).toBe(xyzUrl);
+    expect(xyzLink.prop("title")).toBe(xyzUrl);
+    const abc = links.at(1);
+    expect(abc.prop("href")).toBe(abcUrl);
+    expect(abc.prop("title")).toBe(abcUrl);
+  });
+
+  it("renders full URL if smaller than cropLimit", () => {
+    const xyzUrl = "http://example.com/";
+
+    const openLink = jest.fn();
+    const element = renderRep(xyzUrl, {
+      openLink,
+      useQuotes: false,
+      urlCropLimit: 20
+    });
+
+    expect(element.text()).toEqual(xyzUrl);
+    const link = element.find("a").at(0);
+    expect(link.prop("href")).toBe(xyzUrl);
+    expect(link.prop("title")).toBe(xyzUrl);
+  });
+
+  it("renders cropped URL followed by cropped string with urlCropLimit", () => {
+    const text = "http://example.fr abcdefghijkl";
+    const openLink = jest.fn();
+    const element = renderRep(text, {
+      openLink,
+      useQuotes: false,
+      cropLimit: 20
+    });
+
+    expect(element.text()).toEqual("http://exa…cdefghijkl");
+    const linkFr = element.find("a").at(0);
+    expect(linkFr.prop("href")).toBe("http://example.fr");
+    expect(linkFr.prop("title")).toBe("http://example.fr");
+  });
+
   it("does not render a link if the URL has no scheme", () => {
     const url = "example.com";
     const element = renderRep(url, { useQuotes: false });
     expect(element.text()).toEqual(url);
     expect(element.find("a").exists()).toBeFalsy();
   });
 
   it("does not render a link if the URL has an invalid scheme", () => {
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -36,17 +36,16 @@ support-files =
   doc_style_editor_link.css
   doc_test_image.png
   doc_urls_clickable.css
   doc_urls_clickable.html
   doc_variables_1.html
   doc_variables_2.html
   doc_variables_3.html
   head.js
-  !/devtools/client/debugger/test/mochitest/helpers/context.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/shared-head.js
   !/devtools/client/shared/test/telemetry-test-helpers.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_rules_add-property-and-reselect.js]
--- a/devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js
@@ -142,16 +142,18 @@ const AFTER = [
         },
         ruleIndex: 1,
       },
     ],
   },
 ];
 
 add_task(async function() {
+  await pushPref("devtools.inspector.inactive.css.enabled", true);
+
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   const {inspector, view} = await openRuleView();
 
   await runInactiveCSSTests(view, inspector, BEFORE);
 
   // Toggle `display:flex` to disabled.
   await toggleDeclaration(inspector, view, 0, {
     display: "flex",
--- a/devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js
@@ -144,16 +144,18 @@ const AFTER = [
         },
         ruleIndex: 1,
       },
     ],
   },
 ];
 
 add_task(async function() {
+  await pushPref("devtools.inspector.inactive.css.enabled", true);
+
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   const {inspector, view} = await openRuleView();
 
   await runInactiveCSSTests(view, inspector, BEFORE);
 
   // Toggle `display:grid` to disabled.
   await toggleDeclaration(inspector, view, 0, {
     display: "grid",
--- a/devtools/client/inspector/test/browser_inspector_highlighter-07.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-07.js
@@ -1,13 +1,18 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
+/* import-globals-from ../../debugger/test/mochitest/helpers/context.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/helpers/context.js",
+  this);
+
 // Test that the highlighter works when the debugger is paused.
 
 function debuggerIsPaused(dbg) {
   return !!dbg.selectors.getIsPaused(dbg.selectors.getCurrentThread());
 }
 
 function waitForPaused(dbg) {
   return new Promise(resolve => {
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -17,22 +17,16 @@ Services.scriptloader.loadSubScript(
 //   Services.prefs.clearUserPref("devtools.debugger.log");
 // });
 
 // Import helpers for the inspector that are also shared with others
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
   this);
 
-// Import helpers for the new debugger
-/* import-globals-from ../../debugger/test/mochitest/helpers/context.js */
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/helpers/context.js",
-  this);
-
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
       new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
 });
 
--- a/devtools/client/locales/en-US/accessibility.properties
+++ b/devtools/client/locales/en-US/accessibility.properties
@@ -168,8 +168,22 @@ accessibility.badge.contrast=contrast
 # row in the accessibility tree for a given accessible object that does not
 # satisfy the WCAG guideline for colour contrast.
 accessibility.badge.contrast.tooltip=Does not meet WCAG standards for accessible text.
 
 # LOCALIZATION NOTE (accessibility.tree.filters): A title text for the toolbar
 # within the main accessibility panel that contains a list of filters to be for
 # accessibility audit.
 accessibility.tree.filters=Check for issues:
+
+# LOCALIZATION NOTE (accessibility.progress.initializing): A title text for the
+# accessibility panel overlay shown when accessibility audit is starting up.
+accessibility.progress.initializing=Initializing…
+
+# LOCALIZATION NOTE (accessibility.progress.initializing): A title text for the
+# accessibility panel overlay shown when accessibility audit is running showing
+# the number of nodes being audited. Semi-colon list of plural forms. See:
+# http://developer.mozilla.org/en/docs/Localization_and_Plurals
+accessibility.progress.progressbar=Checking #1 node;Checking #1 nodes
+
+# LOCALIZATION NOTE (accessibility.progress.finishing): A title text for the
+# accessibility panel overlay shown when accessibility audit is finishing up.
+accessibility.progress.finishing=Finishing up…
--- a/devtools/client/shared/components/reps/reps.js
+++ b/devtools/client/shared/components/reps/reps.js
@@ -3742,17 +3742,17 @@ const validProtocols = /(http|https|ftp|
 
 // URL Regex, common idioms:
 //
 // Lead-in (URL):
 // (                     Capture because we need to know if there was a lead-in
 //                       character so we can include it as part of the text
 //                       preceding the match. We lack look-behind matching.
 //  ^|                   The URL can start at the beginning of the string.
-//  [\s(,;'"`]           Or whitespace or some punctuation that does not imply
+//  [\s(,;'"`“]          Or whitespace or some punctuation that does not imply
 //                       a context which would preclude a URL.
 // )
 //
 // We do not need a trailing look-ahead because our regex's will terminate
 // because they run out of characters they can eat.
 
 // What we do not attempt to have the regexp do:
 // - Avoid trailing '.' and ')' characters.  We let our greedy match absorb
@@ -3783,17 +3783,17 @@ const validProtocols = /(http|https|ftp|
 //                       found here: http://www.iana.org/domains/root/db
 //  )
 //  [-\w.!~*'();,/?:@&=+$#%]*
 //                       path onwards. We allow the set of characters that
 //                       encodeURI does not escape plus the result of escaping
 //                       (so also '%')
 // )
 // eslint-disable-next-line max-len
-const urlRegex = /(^|[\s(,;'"`])((?:https?:\/\/|www\d{0,3}[.][a-z0-9.\-]{2,249}|[a-z0-9.\-]{2,250}[.][a-z]{2,4}\/)[-\w.!~*'();,/?:@&=+$#%]*)/im;
+const urlRegex = /(^|[\s(,;'"`“])((?:https?:\/\/|www\d{0,3}[.][a-z0-9.\-]{2,249}|[a-z0-9.\-]{2,250}[.][a-z]{2,4}\/)[-\w.!~*'();,/?:@&=+$#%]*)/im;
 
 // Set of terminators that are likely to have been part of the context rather
 // than part of the URL and so should be uneaten. This is '(', ',', ';', plus
 // quotes and question end-ing punctuation and the potential permutations with
 // parentheses (english-specific).
 const uneatLastUrlCharsRegex = /(?:[),;.!?`'"]|[.!?]\)|\)[.!?])$/;
 
 const ELLIPSIS = "\u2026";
@@ -4391,29 +4391,31 @@ const { a, span } = dom;
 /**
  * Renders a string. String value is enclosed within quotes.
  */
 StringRep.propTypes = {
   useQuotes: PropTypes.bool,
   escapeWhitespace: PropTypes.bool,
   style: PropTypes.object,
   cropLimit: PropTypes.number.isRequired,
+  urlCropLimit: PropTypes.number,
   member: PropTypes.object,
   object: PropTypes.object.isRequired,
   openLink: PropTypes.func,
   className: PropTypes.string,
   title: PropTypes.string,
   isInContentPage: PropTypes.bool
 };
 
 function StringRep(props) {
   const {
     className,
     style,
     cropLimit,
+    urlCropLimit,
     object,
     useQuotes = true,
     escapeWhitespace = true,
     member,
     openLink,
     title,
     isInContentPage
   } = props;
@@ -4445,17 +4447,23 @@ function StringRep(props) {
     className,
     style,
     actor: object.actor,
     title
   });
 
   if (!isLong) {
     if (containsURL(text)) {
-      return span(config, ...getLinkifiedElements(text, shouldCrop && cropLimit, openLink, isInContentPage));
+      return span(config, getLinkifiedElements({
+        text,
+        cropLimit: shouldCrop ? cropLimit : null,
+        urlCropLimit,
+        openLink,
+        isInContentPage
+      }));
     }
 
     // Cropping of longString has been handled before formatting.
     text = maybeCropString({
       isLong,
       shouldCrop,
       cropLimit
     }, text);
@@ -4515,24 +4523,34 @@ function maybeCropString(opts, text) {
 
   return shouldCrop ? rawCropString(text, cropLimit) : text;
 }
 
 /**
  * Get an array of the elements representing the string, cropped if needed,
  * with actual links.
  *
- * @param {String} text: The actual string to linkify.
- * @param {Integer | null} cropLimit
- * @param {Function} openLink: Function handling the link opening.
- * @param {Boolean} isInContentPage: pass true if the reps is rendered in
- *                                   the content page (e.g. in JSONViewer).
+ * @param {Object} An options object of the following shape:
+ *                 - text {String}: The actual string to linkify.
+ *                 - cropLimit {Integer}: The limit to apply on the whole text.
+ *                 - urlCropLimit {Integer}: The limit to apply on each URL.
+ *                 - openLink {Function} openLink: Function handling the link
+ *                                                 opening.
+ *                 - isInContentPage {Boolean}: pass true if the reps is
+ *                                              rendered in the content page
+ *                                              (e.g. in JSONViewer).
  * @returns {Array<String|ReactElement>}
  */
-function getLinkifiedElements(text, cropLimit, openLink, isInContentPage) {
+function getLinkifiedElements({
+  text,
+  cropLimit,
+  urlCropLimit,
+  openLink,
+  isInContentPage
+}) {
   const halfLimit = Math.ceil((cropLimit - ELLIPSIS.length) / 2);
   const startCropIndex = cropLimit ? halfLimit : null;
   const endCropIndex = cropLimit ? text.length - halfLimit : null;
 
   const items = [];
   let currentIndex = 0;
   let contentStart;
   while (true) {
@@ -4552,20 +4570,26 @@ function getLinkifiedElements(text, crop
     // URL.
     let useUrl = url[2];
     const uneat = uneatLastUrlCharsRegex.exec(useUrl);
     if (uneat) {
       useUrl = useUrl.substring(0, uneat.index);
     }
 
     currentIndex = currentIndex + contentStart;
-    const linkText = getCroppedString(useUrl, currentIndex, startCropIndex, endCropIndex);
+    let linkText = getCroppedString(useUrl, currentIndex, startCropIndex, endCropIndex);
 
     if (linkText) {
+      if (urlCropLimit && useUrl.length > urlCropLimit) {
+        const urlCropHalf = Math.ceil((urlCropLimit - ELLIPSIS.length) / 2);
+        linkText = getCroppedString(useUrl, 0, urlCropHalf, useUrl.length - urlCropHalf);
+      }
+
       items.push(a({
+        key: `${useUrl}-${currentIndex}`,
         className: "url",
         title: useUrl,
         draggable: false,
         // Because we don't want the link to be open in the current
         // panel's frame, we only render the href attribute if `openLink`
         // exists (so we can preventDefault) or if the reps will be
         // displayed in content page (e.g. in the JSONViewer).
         href: openLink || isInContentPage ? useUrl : null,
--- a/devtools/client/themes/boxmodel.css
+++ b/devtools/client/themes/boxmodel.css
@@ -286,16 +286,17 @@
   left: -9px;
 }
 
 /* Legend: displayed inside regions */
 
 .boxmodel-legend {
   position: absolute;
   z-index: 1;
+  cursor: default;
 }
 
 .boxmodel-legend[data-box="margin"] {
   margin-left: 9px;
   margin-top: 4px;
   color: var(--grey-90);
 }
 
@@ -332,28 +333,35 @@
 
 /* Editable fields */
 
 .boxmodel-editable {
   position: relative;
   border: 1px dashed transparent;
   -moz-user-select: none;
   white-space: nowrap;
+  cursor: pointer;
 }
 
 .boxmodel-editable[data-box="border"] {
   background-color: var(--borderbox-color);
   border-radius: 3px;
   padding: 0 2px;
 }
 
 .boxmodel-editable:hover {
   border-bottom-color: hsl(0, 0%, 50%);
 }
 
+.boxmodel-editable:focus,
+.boxmodel-editable:active {
+  border: 1px solid var(--blue-50);
+  outline: none;
+}
+
 .boxmodel-editable[data-box="margin"]:hover {
   background-color: var(--marginbox-border-color);
 }
 
 .boxmodel-editable[data-box="padding"]:hover {
   background-color: #c78fc7b3;
 }
 
--- a/devtools/client/webconsole/components/message-types/PageError.js
+++ b/devtools/client/webconsole/components/message-types/PageError.js
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // React & Redux
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const Message = createFactory(require("devtools/client/webconsole/components/Message"));
+const { MODE, REPS } = require("devtools/client/shared/components/reps/reps");
 
 PageError.displayName = "PageError";
 
 PageError.propTypes = {
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
   timestampsVisible: PropTypes.bool.isRequired,
   serviceContainer: PropTypes.object,
@@ -47,22 +48,24 @@ function PageError(props) {
     messageText,
     stacktrace,
     frame,
     exceptionDocURL,
     timeStamp,
     notes,
   } = message;
 
-  let messageBody;
-  if (typeof messageText === "string") {
-    messageBody = messageText;
-  } else if (typeof messageText === "object" && messageText.type === "longString") {
-    messageBody = `${message.messageText.initial}…`;
-  }
+  const messageBody = REPS.StringRep.rep({
+    object: messageText,
+    mode: MODE.LONG,
+    useQuotes: false,
+    escapeWhitespace: false,
+    urlCropLimit: 120,
+    openLink: serviceContainer.openLink,
+  });
 
   return Message({
     dispatch,
     messageId,
     executionPoint,
     isPaused,
     open,
     collapsible: Array.isArray(stacktrace),
--- a/devtools/client/webconsole/test/components/evaluation-result.test.js
+++ b/devtools/client/webconsole/test/components/evaluation-result.test.js
@@ -1,19 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test utils.
 const expect = require("expect");
-const { render } = require("enzyme");
-
-// Commented out for "displays a [Learn more] link"
-// const { render, mount } = require("enzyme");
-// const sinon = require("sinon");
+const { render, mount } = require("enzyme");
+const sinon = require("sinon");
 
 // React
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const Provider = createFactory(require("react-redux").Provider);
 const { setupStore } = require("devtools/client/webconsole/test/helpers");
 
 // Components under test.
 const EvaluationResult = createFactory(
@@ -95,37 +92,40 @@ describe("EvaluationResult component:", 
     const message = stubPreparedMessages.get("cd(document)");
     const wrapper = render(EvaluationResult({ message, serviceContainer }));
 
     expect(wrapper.find(".message-body").text()).toBe(
       "Cannot cd() to the given window. Invalid argument."
     );
   });
 
-  // TODO: Regressed by Bug 1230194, disabled in Bug 1535484. Filed Bug 1550791 to fix it.
-  // it("displays a [Learn more] link", () => {
-  //   const store = setupStore();
+  it("displays a [Learn more] link", () => {
+    const store = setupStore();
 
-  //   const message = stubPreparedMessages.get("asdf()");
-
-  //   serviceContainer.openLink = sinon.spy();
-  //   const wrapper = mount(
-  //     Provider({ store }, EvaluationResult({ message, serviceContainer }))
-  //   );
+    const message = stubPreparedMessages.get("asdf()");
 
-  //   const url =
-  //     "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined";
-  //   const learnMore = wrapper.find(".learn-more-link");
-  //   expect(learnMore.length).toBe(1);
-  //   expect(learnMore.prop("title")).toBe(url);
+    serviceContainer.openLink = sinon.spy();
+    const wrapper = mount(
+      Provider({ store }, EvaluationResult({
+        message,
+        serviceContainer,
+        dispatch: () => {},
+      }))
+    );
 
-  //   learnMore.simulate("click");
-  //   const call = serviceContainer.openLink.getCall(0);
-  //   expect(call.args[0]).toEqual(message.exceptionDocURL);
-  // });
+    const url =
+      "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined";
+    const learnMore = wrapper.find(".learn-more-link");
+    expect(learnMore.length).toBe(1);
+    expect(learnMore.prop("title")).toBe(url);
+
+    learnMore.simulate("click");
+    const call = serviceContainer.openLink.getCall(0);
+    expect(call.args[0]).toEqual(message.exceptionDocURL);
+  });
 
   it("has the expected indent", () => {
     const message = stubPreparedMessages.get("new Date(0)");
 
     const indent = 10;
     // We need to wrap the ConsoleApiElement in a Provider in order for the
     // ObjectInspector to work.
     let wrapper = render(
--- a/devtools/client/webconsole/test/components/page-error.test.js
+++ b/devtools/client/webconsole/test/components/page-error.test.js
@@ -6,27 +6,29 @@
 const expect = require("expect");
 const { render, mount } = require("enzyme");
 const sinon = require("sinon");
 
 // React
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const Provider = createFactory(require("react-redux").Provider);
 const { setupStore } = require("devtools/client/webconsole/test/helpers");
+const { prepareMessage } = require("devtools/client/webconsole/utils/messages");
 
 // Components under test.
 const PageError = require("devtools/client/webconsole/components/message-types/PageError");
 const {
   MESSAGE_OPEN,
   MESSAGE_CLOSE,
 } = require("devtools/client/webconsole/constants");
 const { INDENT_WIDTH } = require("devtools/client/webconsole/components/MessageIndent");
 
 // Test fakes.
-const { stubPreparedMessages } = require("devtools/client/webconsole/test/fixtures/stubs/index");
+const { stubPackets, stubPreparedMessages } =
+  require("devtools/client/webconsole/test/fixtures/stubs/index");
 const serviceContainer = require("devtools/client/webconsole/test/fixtures/serviceContainer");
 
 describe("PageError component:", () => {
   it("renders", () => {
     const message = stubPreparedMessages.get("ReferenceError: asdf is not defined");
     const wrapper = render(PageError({
       message,
       serviceContainer,
@@ -78,16 +80,61 @@ describe("PageError component:", () => {
 
   it("renders thrown string", () => {
     const message = stubPreparedMessages.get(`throw "tomato"`);
     const wrapper = render(PageError({ message, serviceContainer }));
     const text = wrapper.find(".message-body").text();
     expect(text).toBe(`uncaught exception: tomato`);
   });
 
+  it("renders URLs in message as actual, cropped, links", () => {
+    // Let's replace the packet data in order to mimick a pageError.
+    const packet = stubPackets.get("ReferenceError: asdf is not defined");
+
+    const evilDomain = `https://evil.com/?`;
+    const badDomain = `https://not-so-evil.com/?`;
+    const paramLength = 200;
+    const longParam = "a".repeat(paramLength);
+
+    const evilURL = `${evilDomain}${longParam}`;
+    const badURL = `${badDomain}${longParam}`;
+
+    packet.pageError.errorMessage =
+      `“${evilURL}“ is evil and “${badURL}“ is not good either`;
+
+    // We remove the exceptionDocURL to not have the "learn more" link.
+    packet.pageError.exceptionDocURL = null;
+
+    const message = prepareMessage(packet, {getNextId: () => "1"});
+    const wrapper = render(PageError({ message, serviceContainer }));
+
+    // Keep in sync with `urlCropLimit` in PageError.js.
+    const cropLimit = 120;
+    const partLength = cropLimit / 2;
+    const getCroppedUrl = url =>
+      `${url}${"a".repeat((partLength - url.length))}…${"a".repeat(partLength)}`;
+
+    const croppedEvil = getCroppedUrl(evilDomain);
+    const croppedbad = getCroppedUrl(badDomain);
+
+    const text = wrapper.find(".message-body").text();
+    expect(text).toBe(
+      `“${croppedEvil}“ is evil and “${croppedbad}“ is not good either`);
+
+    // There should be 2 links.
+    const links = wrapper.find(".message-body a");
+    expect(links.length).toBe(2);
+
+    expect(links.eq(0).attr("href")).toBe(evilURL);
+    expect(links.eq(0).attr("title")).toBe(evilURL);
+
+    expect(links.eq(1).attr("href")).toBe(badURL);
+    expect(links.eq(1).attr("title")).toBe(badURL);
+  });
+
   it("displays a [Learn more] link", () => {
     const store = setupStore();
 
     const message = stubPreparedMessages.get("ReferenceError: asdf is not defined");
 
     serviceContainer.openLink = sinon.spy();
     const wrapper = mount(Provider({store},
       PageError({
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -291,16 +291,17 @@ tags = clipboard
 [browser_webconsole_csp_ignore_reflected_xss_message.js]
 [browser_webconsole_csp_violation.js]
 [browser_webconsole_cspro.js]
 [browser_webconsole_document_focus.js]
 [browser_webconsole_duplicate_errors.js]
 [browser_webconsole_error_with_grouped_stack.js]
 [browser_webconsole_error_with_longstring_stack.js]
 [browser_webconsole_error_with_unicode.js]
+[browser_webconsole_error_with_url.js]
 [browser_webconsole_errors_after_page_reload.js]
 [browser_webconsole_eval_error.js]
 [browser_webconsole_eval_in_debugger_stackframe.js]
 [browser_webconsole_eval_in_debugger_stackframe2.js]
 [browser_webconsole_eval_sources.js]
 [browser_webconsole_execution_scope.js]
 [browser_webconsole_external_script_errors.js]
 [browser_webconsole_file_uri.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_error_with_url.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check if an error with Unicode characters is reported correctly.
+
+"use strict";
+
+const longParam = "0".repeat(200);
+const url1 = `https://example.com?v=${longParam}`;
+const url2 = `https://example.org?v=${longParam}`;
+
+const TEST_URI = `data:text/html;charset=utf8,<script>
+  throw "Visit \u201c${url1}\u201d or \u201c${url2}\u201d to get more " +
+        "information on this error.";
+</script>`;
+const {ELLIPSIS} = require("devtools/shared/l10n");
+
+add_task(async function() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  // On e10s, the exception is triggered in child process
+  // and is ignored by test harness
+  if (!Services.appinfo.browserTabsRemoteAutostart) {
+    expectUncaughtException();
+  }
+
+  const getCroppedUrl = origin => {
+    const cropLimit = 120;
+    const half = cropLimit / 2;
+    const params =
+      `?v=${"0".repeat(half - origin.length - 3)}${ELLIPSIS}${"0".repeat(half)}`;
+    return `${origin}${params}`;
+  };
+
+  const EXPECTED_MESSAGE = `get more information on this error`;
+
+  const msg = await waitFor(() => findMessage(hud, EXPECTED_MESSAGE));
+  ok(msg, `Link in error message are cropped as expected`);
+
+  const [comLink, orgLink] = Array.from(msg.querySelectorAll("a"));
+  is(comLink.getAttribute("href"), url1, "First link has expected url");
+  is(comLink.getAttribute("title"), url1, "First link has expected tooltip");
+  is(comLink.textContent, getCroppedUrl("https://example.com"),
+    "First link has expected text");
+
+  is(orgLink.getAttribute("href"), url2, "Second link has expected url");
+  is(orgLink.getAttribute("title"), url2, "Second link has expected tooltip");
+  is(orgLink.textContent, getCroppedUrl("https://example.org"),
+    "Second link has expected text");
+});
--- a/devtools/docs/contributing/performance.md
+++ b/devtools/docs/contributing/performance.md
@@ -131,17 +131,17 @@ It highlights that:
 ### Run performance tests
 
 See if any subtest reports a improvement. Ensure that the improvement makes any sense.
 For example, if the test is 50% faster, maybe you broke the performance test.
 This might happen if the test no longer waits for all the operations to finish executing before completing.
 
 To push your current patch to try, execute:
 ```
-./mach try -b o -p linux64 -u none -t damp-e10s --rebuild-talos 5 --artifact
+./mach try -b o -p linux64 -u none -t damp --rebuild-talos 5 --artifact
 ```
 It will print in your Terminal a link to perfherder like this one:
 [https://treeherder.mozilla.org/perf.html#/comparechooser?newProject=try&newRevision=9bef6cb13c43bbce21d40ffaea595e082a4c28db](https://treeherder.mozilla.org/perf.html#/comparechooser?newProject=try&newRevision=9bef6cb13c43bbce21d40ffaea595e082a4c28db)
 Running performance tests takes time, so you should open it 30 minutes up to 2 hours later to see your results.
 See [Performance tests (DAMP)](../tests/performance-tests.md) for more information about PerfHerder/try.
 
 Let's look at how to interpret an actual real-life [set of perfherder results](https://treeherder.mozilla.org/perf.html#/comparesubtest?originalProject=mozilla-central&newProject=try&newRevision=9bef6cb13c43bbce21d40ffaea595e082a4c28db&originalSignature=edaec66500db21d37602c99daa61ac983f21a6ac&newSignature=edaec66500db21d37602c99daa61ac983f21a6ac&showOnlyImportant=1&framework=1&selectedTimeRange=172800):
 
--- a/devtools/docs/tests/performance-tests.md
+++ b/devtools/docs/tests/performance-tests.md
@@ -12,21 +12,21 @@ This will run all DAMP tests, you can fi
 ```bash
 ./mach talos-test --activeTests damp --subtests console
 ```
 This command will run all tests which contains "console" in their name.
 
 ## How to run it on try?
 
 ```bash
-./mach try -b o -p linux64 -u none -t damp-e10s --rebuild-talos 6
+./mach try -b o -p linux64 -u none -t damp --rebuild-talos 6
 ```
 * Linux appears to build and run quickly, and offers quite stable results over the other OSes.
 The vast majority of performance issues for DevTools are OS agnostic, so it doesn't really matter which one you run them on.
-* "damp-e10s" is the talos bucket in which we run DAMP.
+* "damp" is the talos bucket in which we run DAMP.
 * And 6 is the number of times we run DAMP tests. That's to do averages between all the 6 runs and helps filtering out the noise.
 
 ## What does it do?
 
 DAMP measures three important operations:
 * Open a toolbox
 * Reload the web page
 * Close the toolbox
--- a/devtools/docs/tests/tips.md
+++ b/devtools/docs/tests/tips.md
@@ -13,10 +13,10 @@ export MOZ_QUIET=1
 
 You can also send `MOZ_QUIET` when you push to try&hellip; it makes the logs easier to read and makes the tests run faster because there is so much less logging.
 
 Example try syntax containing `MOZ_QUIET`:
 
 ```
 ./mach try -b do -p linux,linux64,macosx64,win32,win64 \
   -u xpcshell,mochitest-bc,mochitest-e10s-bc,mochitest-dt,mochitest-chrome \
-  -t damp-e10s --setenv MOZ_QUIET=1
+  -t damp --setenv MOZ_QUIET=1
 ```
--- a/devtools/server/actors/accessibility/walker.js
+++ b/devtools/server/actors/accessibility/walker.js
@@ -125,27 +125,77 @@ function isStale(accessible) {
 
 /**
  * Get accessibility audit starting with the passed accessible object as a root.
  *
  * @param {Object} acc
  *        AccessibileActor to be used as the root for the audit.
  * @param {Map} report
  *        An accumulator map to be used to store audit information.
+ * @param {Object} progress
+ *        An audit project object that is used to track the progress of the
+ *        audit and send progress "audit-event" events to the client.
  */
-function getAudit(acc, report) {
+function getAudit(acc, report, progress) {
   if (acc.isDefunct) {
     return;
   }
 
   // Audit returns a promise, save the actual value in the report.
-  report.set(acc, acc.audit().then(result => report.set(acc, result)));
+  report.set(acc, acc.audit().then(result => {
+    report.set(acc, result);
+    progress.increment();
+  }));
 
   for (const child of acc.children()) {
-    getAudit(child, report);
+    getAudit(child, report, progress);
+  }
+}
+
+/**
+ * A helper class that is used to track audit progress and send progress events
+ * to the client.
+ */
+class AuditProgress {
+  constructor(walker) {
+    this.completed = 0;
+    this.percentage = 0;
+    this.walker = walker;
+  }
+
+  setTotal(size) {
+    this.size = size;
+  }
+
+  notify() {
+    this.walker.emit("audit-event", {
+      type: "progress",
+      progress: {
+        total: this.size,
+        percentage: this.percentage,
+      },
+    });
+  }
+
+  increment() {
+    this.completed++;
+    const { completed, size } = this;
+    if (!size) {
+      return;
+    }
+
+    const percentage = Math.round(completed / size * 100);
+    if (percentage > this.percentage) {
+      this.percentage = percentage;
+      this.notify();
+    }
+  }
+
+  destroy() {
+    this.walker = null;
   }
 }
 
 /**
  * The AccessibleWalkerActor stores a cache of AccessibleActors that represent
  * accessible objects in a given document.
  *
  * It is also responsible for implicitely initializing and shutting down
@@ -396,17 +446,19 @@ const AccessibleWalkerActor = ActorClass
    *
    * @return {Promise}
    *         A promise that resolves when the audit is complete and all relevant
    *         ancestries are calculated.
    */
   async audit() {
     const doc = await this.getDocument();
     const report = new Map();
-    getAudit(doc, report);
+    this._auditProgress = new AuditProgress(this);
+    getAudit(doc, report, this._auditProgress);
+    this._auditProgress.setTotal(report.size);
     await Promise.all(report.values());
 
     const ancestries = [];
     for (const [acc, audit] of report.entries()) {
       // Filter out audits that have no failing checks.
       if (audit &&
           Object.values(audit).some(check => check != null && !check.error &&
             check.score === accessibility.SCORES.FAIL)) {
@@ -426,20 +478,25 @@ const AccessibleWalkerActor = ActorClass
     // Audit is already running, wait for the "audit-event" event.
     if (this._auditing) {
       return;
     }
 
     this._auditing = this.audit()
       // We do not want to block on audit request, instead fire "audit-event"
       // event when internal audit is finished or failed.
-      .then(ancestries => this.emit("audit-event", { ancestries }))
-      .catch(() => this.emit("audit-event", { error: true }))
+      .then(ancestries => this.emit("audit-event", {
+        type: "completed",
+        ancestries,
+      }))
+      .catch(() => this.emit("audit-event", { type: "error" }))
       .finally(() => {
         this._auditing = null;
+        this._auditProgress.destroy();
+        this._auditProgress = null;
       });
   },
 
   onHighlighterEvent: function(data) {
     this.emit("highlighter-event", data);
   },
 
   /**
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -133,16 +133,20 @@ const proto = {
 
     // FF40+: Allow to know how many properties an object has to lazily display them
     // when there is a bunch.
     if (isTypedArray(g)) {
       // Bug 1348761: getOwnPropertyNames is unnecessary slow on TypedArrays
       g.ownPropertyLength = getArrayLength(this.obj);
     } else if (isStorage(g)) {
       g.ownPropertyLength = getStorageLength(this.obj);
+    } else if (isReplaying) {
+      // When replaying we can get the number of properties directly, to avoid
+      // needing to enumerate all of them.
+      g.ownPropertyLength = this.obj.getOwnPropertyNamesCount();
     } else {
       try {
         g.ownPropertyLength = this.obj.getOwnPropertyNames().length;
       } catch (err) {
         // The above can throw when the debuggee does not subsume the object's
         // compartment, or for some WrappedNatives like Cu.Sandbox.
       }
     }
@@ -330,16 +334,23 @@ const proto = {
     let obj = this.obj;
     let level = 0, i = 0;
 
     // Do not search safe getters in unsafe objects.
     if (!DevToolsUtils.isSafeDebuggerObject(obj)) {
       return safeGetterValues;
     }
 
+    // Do not search for safe getters while replaying. While this would be nice
+    // to support, it involves a lot of back-and-forth between processes and
+    // would be better to do entirely in the replaying process.
+    if (isReplaying) {
+      return safeGetterValues;
+    }
+
     // Most objects don't have any safe getters but inherit some from their
     // prototype. Avoid calling getOwnPropertyNames on objects that may have
     // many properties like Array, strings or js objects. That to avoid
     // freezing firefox when doing so.
     if (isArray(this.obj) || ["Object", "String"].includes(this.obj.class)) {
       obj = obj.proto;
       level++;
     }
--- a/devtools/server/actors/object/previewers.js
+++ b/devtools/server/actors/object/previewers.js
@@ -372,16 +372,20 @@ function GenericObject(objectActor, grip
     if (ObjectUtils.isStorage(obj)) {
       // local and session storage cannot be iterated over using
       // Object.getOwnPropertyNames() because it skips keys that are duplicated
       // on the prototype e.g. "key", "getKeys" so we need to gather the real
       // keys using the storage.key() function.
       for (let j = 0; j < rawObj.length; j++) {
         names.push(rawObj.key(j));
       }
+    } else if (isReplaying) {
+      // When replaying we can access a batch of properties for use in generating
+      // the preview. This avoids needing to enumerate all properties.
+      names = obj.getEnumerableOwnPropertyNamesForPreview();
     } else {
       names = obj.getOwnPropertyNames();
     }
     symbols = obj.getOwnPropertySymbols();
   } catch (ex) {
     // Calling getOwnPropertyNames() on some wrapped native prototypes is not
     // allowed: "cannot modify properties of a WrappedNative". See bug 952093.
   }
@@ -776,16 +780,22 @@ previewers.Object = [
 
   function PseudoArray({obj, hooks}, grip, rawObj) {
     // An object is considered a pseudo-array if all the following apply:
     // - All its properties are array indices except, optionally, a "length" property.
     // - At least it has the "0" array index.
     // - The array indices are consecutive.
     // - The value of "length", if present, is the number of array indices.
 
+    // Don't generate pseudo array previews when replaying. We don't want to
+    // have to enumerate all the properties in order to determine this.
+    if (isReplaying) {
+      return false;
+    }
+
     let keys;
     try {
       keys = obj.getOwnPropertyNames();
     } catch (err) {
       // The above can throw when the debuggee does not subsume the object's
       // compartment, or for some WrappedNatives like Cu.Sandbox.
       return false;
     }
--- a/devtools/server/actors/replay/debugger.js
+++ b/devtools/server/actors/replay/debugger.js
@@ -189,31 +189,33 @@ ReplayDebugger.prototype = {
     this._ensurePaused();
     this._setResume(() => {
       this._direction = forward ? Direction.FORWARD : Direction.BACKWARD;
       dumpv("Resuming " + this._direction);
       this._control.resume(forward);
       if (this._paused) {
         // If we resume and immediately pause, we are at an endpoint of the
         // recording. Force the thread to pause.
+        this._capturePauseData();
         this.replayingOnForcedPause(this.getNewestFrame());
       }
     });
   },
 
   replayTimeWarp(target) {
     this._ensurePaused();
     this._setResume(() => {
       this._direction = Direction.NONE;
       dumpv("Warping " + JSON.stringify(target));
       this._control.timeWarp(target);
 
       // timeWarp() doesn't return until the child has reached the target of
       // the warp, after which we force the thread to pause.
       assert(this._paused);
+      this._capturePauseData();
       this.replayingOnForcedPause(this.getNewestFrame());
     });
   },
 
   replayPause() {
     this._ensurePaused();
 
     // Cancel any pending resume.
@@ -347,16 +349,58 @@ ReplayDebugger.prototype = {
   _invalidateAfterUnpause() {
     this._frames.forEach(frame => frame._invalidate());
     this._frames.length = 0;
 
     this._objects.forEach(obj => obj._invalidate());
     this._objects.length = 0;
   },
 
+  // Fill in the debugger with (hopefully) all data the client/server need to
+  // pause at the current location.
+  _capturePauseData() {
+    if (this._frames.length) {
+      return;
+    }
+
+    const pauseData = this._sendRequestAllowDiverge({ type: "pauseData" });
+    if (!pauseData.frames) {
+      return;
+    }
+
+    for (const data of Object.values(pauseData.scripts)) {
+      this._addScript(data);
+    }
+
+    for (const { scriptId, offset, metadata} of pauseData.offsetMetadata) {
+      if (this._scripts[scriptId]) {
+        const script = this._getScript(scriptId);
+        script._addOffsetMetadata(offset, metadata);
+      }
+    }
+
+    for (const { data, preview } of Object.values(pauseData.objects)) {
+      if (!this._objects[data.id]) {
+        this._addObject(data);
+      }
+      this._getObject(data.id)._preview = preview;
+    }
+
+    for (const { data, names } of Object.values(pauseData.environments)) {
+      if (!this._objects[data.id]) {
+        this._addObject(data);
+      }
+      this._getObject(data.id)._names = names;
+    }
+
+    for (const frame of pauseData.frames) {
+      this._frames[frame.index] = new ReplayDebuggerFrame(this, frame);
+    }
+  },
+
   /////////////////////////////////////////////////////////
   // Search management
   /////////////////////////////////////////////////////////
 
   _forEachSearch(callback) {
     for (const { position } of this._searches) {
       callback(position);
     }
@@ -551,30 +595,34 @@ ReplayDebugger.prototype = {
 
   /////////////////////////////////////////////////////////
   // Object methods
   /////////////////////////////////////////////////////////
 
   _getObject(id) {
     if (id && !this._objects[id]) {
       const data = this._sendRequest({ type: "getObject", id });
-      switch (data.kind) {
-      case "Object":
-        this._objects[id] = new ReplayDebuggerObject(this, data);
-        break;
-      case "Environment":
-        this._objects[id] = new ReplayDebuggerEnvironment(this, data);
-        break;
-      default:
-        ThrowError("Unknown object kind");
-      }
+      this._addObject(data);
     }
     return this._objects[id];
   },
 
+  _addObject(data) {
+    switch (data.kind) {
+    case "Object":
+      this._objects[data.id] = new ReplayDebuggerObject(this, data);
+      break;
+    case "Environment":
+      this._objects[data.id] = new ReplayDebuggerEnvironment(this, data);
+      break;
+    default:
+      ThrowError("Unknown object kind");
+    }
+  },
+
   // Convert a value we received from the child.
   _convertValue(value) {
     if (isNonNullObject(value)) {
       if (value.object) {
         return this._getObject(value.object);
       }
       if (value.snapshot) {
         return new ReplayDebuggerObjectSnapshot(this, value.snapshot);
@@ -689,18 +737,20 @@ ReplayDebugger.prototype = {
   get replayingOnPopFrame() {
     return this._searchBreakpoints(({position, data}) => {
       return (position.kind == "OnPop" && !position.script) ? data : null;
     });
   },
 
   set replayingOnPopFrame(handler) {
     if (handler) {
-      this._setBreakpoint(() => { handler.call(this, this.getNewestFrame()); },
-                          { kind: "OnPop" }, handler);
+      this._setBreakpoint(() => {
+        this._capturePauseData();
+        handler.call(this, this.getNewestFrame());
+      }, { kind: "OnPop" }, handler);
     } else {
       this._clearMatchingBreakpoints(({position}) => {
         return position.kind == "OnPop" && !position.script;
       });
     }
   },
 
   getNewConsoleMessage() {
@@ -722,16 +772,17 @@ ReplayDebugger.prototype = {
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebuggerScript
 ///////////////////////////////////////////////////////////////////////////////
 
 function ReplayDebuggerScript(dbg, data) {
   this._dbg = dbg;
   this._data = data;
+  this._offsetMetadata = [];
 }
 
 ReplayDebuggerScript.prototype = {
   get displayName() { return this._data.displayName; },
   get url() { return this._data.url; },
   get startLine() { return this._data.startLine; },
   get lineCount() { return this._data.lineCount; },
   get source() { return this._dbg._getSource(this._data.sourceId); },
@@ -744,28 +795,39 @@ ReplayDebuggerScript.prototype = {
     return this._dbg._sendRequest({ type, id: this._data.id, value });
   },
 
   getLineOffsets(line) { return this._forward("getLineOffsets", line); },
   getOffsetLocation(pc) { return this._forward("getOffsetLocation", pc); },
   getSuccessorOffsets(pc) { return this._forward("getSuccessorOffsets", pc); },
   getPredecessorOffsets(pc) { return this._forward("getPredecessorOffsets", pc); },
   getAllColumnOffsets() { return this._forward("getAllColumnOffsets"); },
-  getOffsetMetadata(pc) { return this._forward("getOffsetMetadata", pc); },
   getPossibleBreakpoints(query) {
     return this._forward("getPossibleBreakpoints", query);
   },
   getPossibleBreakpointOffsets(query) {
     return this._forward("getPossibleBreakpointOffsets", query);
   },
 
+  getOffsetMetadata(pc) {
+    if (!this._offsetMetadata[pc]) {
+      this._addOffsetMetadata(pc, this._forward("getOffsetMetadata", pc));
+    }
+    return this._offsetMetadata[pc];
+  },
+
+  _addOffsetMetadata(pc, metadata) {
+    this._offsetMetadata[pc] = metadata;
+  },
+
   setBreakpoint(offset, handler) {
-    this._dbg._setBreakpoint(() => { handler.hit(this._dbg.getNewestFrame()); },
-                             { kind: "Break", script: this._data.id, offset },
-                             handler);
+    this._dbg._setBreakpoint(() => {
+      this._dbg._capturePauseData();
+      handler.hit(this._dbg.getNewestFrame());
+    }, { kind: "Break", script: this._data.id, offset }, handler);
   },
 
   clearBreakpoint(handler) {
     this._dbg._clearMatchingBreakpoints(({position, data}) => {
       return position.script == this._data.id && handler == data;
     });
   },
 
@@ -862,35 +924,38 @@ ReplayDebuggerFrame.prototype = {
 
   set onStep(handler) {
     // Use setReplayingOnStep or replayClearSteppingHooks instead.
     NotAllowed();
   },
 
   setReplayingOnStep(handler, offsets) {
     offsets.forEach(offset => {
-      this._dbg._setBreakpoint(
-        () => { handler.call(this._dbg.getNewestFrame()); },
-        { kind: "OnStep",
-          script: this._data.script,
-          offset,
-          frameIndex: this._data.index },
-        handler);
+      this._dbg._setBreakpoint(() => {
+        this._dbg._capturePauseData();
+        handler.call(this._dbg.getNewestFrame());
+      }, {
+        kind: "OnStep",
+        script: this._data.script,
+        offset,
+        frameIndex: this._data.index,
+      }, handler);
     });
   },
 
   get onPop() {
     return this._dbg._searchBreakpoints(({position, data}) => {
       return this._positionMatches(position, "OnPop") ? data : null;
     });
   },
 
   set onPop(handler) {
     if (handler) {
       this._dbg._setBreakpoint(() => {
+          this._dbg._capturePauseData();
           const result = this._dbg._sendRequest({ type: "popFrameResult" });
           handler.call(this._dbg.getNewestFrame(),
                        this._dbg._convertCompletionValue(result));
         },
         { kind: "OnPop", script: this._data.script, frameIndex: this._data.index },
         handler);
     } else {
       // Use replayClearSteppingHooks instead.
@@ -912,25 +977,25 @@ ReplayDebuggerFrame.prototype = {
 
 ///////////////////////////////////////////////////////////////////////////////
 // ReplayDebuggerObject
 ///////////////////////////////////////////////////////////////////////////////
 
 function ReplayDebuggerObject(dbg, data) {
   this._dbg = dbg;
   this._data = data;
+  this._preview = null;
   this._properties = null;
-  this._proxyData = null;
 }
 
 ReplayDebuggerObject.prototype = {
   _invalidate() {
     this._data = null;
+    this._preview = null;
     this._properties = null;
-    this._proxyData = null;
   },
 
   get callable() { return this._data.callable; },
   get isBoundFunction() { return this._data.isBoundFunction; },
   get isArrowFunction() { return this._data.isArrowFunction; },
   get isGeneratorFunction() { return this._data.isGeneratorFunction; },
   get isAsyncFunction() { return this._data.isAsyncFunction; },
   get class() { return this._data.class; },
@@ -951,80 +1016,92 @@ ReplayDebuggerObject.prototype = {
     return null;
   },
 
   getOwnPropertyNames() {
     this._ensureProperties();
     return Object.keys(this._properties);
   },
 
+  getEnumerableOwnPropertyNamesForPreview() {
+    if (this._preview) {
+      return Object.keys(this._preview.enumerableOwnProperties);
+    }
+    return this.getOwnPropertyNames();
+  },
+
+  getOwnPropertyNamesCount() {
+    if (this._preview) {
+      return this._preview.ownPropertyNamesCount;
+    }
+    return this.getOwnPropertyNames().length;
+  },
+
   getOwnPropertySymbols() {
     // Symbol properties are not handled yet.
     return [];
   },
 
   getOwnPropertyDescriptor(name) {
+    if (this._preview) {
+      if (this._preview.enumerableOwnProperties) {
+        const desc = this._preview.enumerableOwnProperties[name];
+        if (desc) {
+          return this._convertPropertyDescriptor(desc);
+        }
+      }
+      if (name == "length") {
+        return this._convertPropertyDescriptor(this._preview.lengthProperty);
+      }
+      if (name == "displayName") {
+        return this._convertPropertyDescriptor(this._preview.displayNameProperty);
+      }
+    }
     this._ensureProperties();
-    const desc = this._properties[name];
-    return desc ? this._convertPropertyDescriptor(desc) : undefined;
+    return this._convertPropertyDescriptor(this._properties[name]);
   },
 
   _ensureProperties() {
     if (!this._properties) {
       const id = this._data.id;
-      const properties =
+      this._properties =
         this._dbg._sendRequestAllowDiverge({ type: "getObjectProperties", id });
-      this._properties = Object.create(null);
-      properties.forEach(({name, desc}) => { this._properties[name] = desc; });
     }
   },
 
   _convertPropertyDescriptor(desc) {
+    if (!desc) {
+      return undefined;
+    }
     const rv = Object.assign({}, desc);
     if ("value" in desc) {
       rv.value = this._dbg._convertValue(desc.value);
     }
     if ("get" in desc) {
       rv.get = this._dbg._getObject(desc.get);
     }
     if ("set" in desc) {
       rv.set = this._dbg._getObject(desc.set);
     }
     return rv;
   },
 
-  _ensureProxyData() {
-    if (!this._proxyData) {
-      const data = this._dbg._sendRequestAllowDiverge({
-        type: "objectProxyData",
-        id: this._data.id,
-      });
-      if (data.exception) {
-        throw new Error(data.exception);
-      }
-      this._proxyData = data;
-    }
-  },
-
   unwrap() {
     if (!this.isProxy) {
       return this;
     }
-    this._ensureProxyData();
-    return this._dbg._convertValue(this._proxyData.unwrapped);
+    return this._dbg._convertValue(this._data.proxyUnwrapped);
   },
 
   get proxyTarget() {
-    this._ensureProxyData();
-    return this._dbg._convertValue(this._proxyData.target);
+    return this._dbg._convertValue(this._data.proxyTarget);
   },
 
   get proxyHandler() {
-    this._ensureProxyData();
-    return this._dbg._convertValue(this._proxyData.handler);
+    return this._dbg._convertValue(this._data.proxyHandler);
   },
 
   get boundTargetFunction() {
     if (this.isBoundFunction) {
       return this._dbg._getObject(this._data.boundTargetFunction);
     }
     return undefined;
   },
--- a/devtools/server/actors/replay/replay.js
+++ b/devtools/server/actors/replay/replay.js
@@ -208,17 +208,17 @@ dbg.onNewScript = function(script) {
 ///////////////////////////////////////////////////////////////////////////////
 
 // Snapshots are generated for objects that might be inspected at times when we
 // are not paused at the point where the snapshot was originally taken. The
 // snapshot data is provided to the server, which can use it to provide limited
 // answers to the client about the object's contents, without having to consult
 // a child process.
 
-function snapshotObjectProperty({ name, desc }) {
+function snapshotObjectProperty([ name, desc ]) {
   // Only capture primitive properties in object snapshots.
   if ("value" in desc && !convertedValueIsObject(desc.value)) {
     return { name, desc };
   }
   return { name, desc: { value: "<unavailable>" } };
 }
 
 function makeObjectSnapshot(object) {
@@ -238,17 +238,17 @@ function makeObjectSnapshot(object) {
     class: object.class,
     name: object.name,
     displayName: object.displayName,
     parameterNames: object.parameterNames,
     isProxy: object.isProxy,
     isExtensible: object.isExtensible(),
     isSealed: object.isSealed(),
     isFrozen: object.isFrozen(),
-    properties: getObjectProperties(object).map(snapshotObjectProperty),
+    properties: Object.entries(getObjectProperties(object)).map(snapshotObjectProperty),
   };
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // Console Message State
 ///////////////////////////////////////////////////////////////////////////////
 
 const gConsoleMessages = [];
@@ -619,62 +619,309 @@ function getSourceData(id) {
     sourceMapURL: source.sourceMapURL,
   };
 }
 
 function forwardToScript(name) {
   return request => gScripts.getObject(request.id)[name](request.value);
 }
 
+function getFrameData(index) {
+  const frame = scriptFrameForIndex(index);
+
+  let _arguments = null;
+  if (frame.arguments) {
+    _arguments = [];
+    for (let i = 0; i < frame.arguments.length; i++) {
+      _arguments.push(convertValue(frame.arguments[i]));
+    }
+  }
+
+  return {
+    index,
+    type: frame.type,
+    callee: getObjectId(frame.callee),
+    environment: getObjectId(frame.environment),
+    generator: frame.generator,
+    constructing: frame.constructing,
+    this: convertValue(frame.this),
+    script: gScripts.getId(frame.script),
+    offset: frame.offset,
+    arguments: _arguments,
+  };
+}
+
 function unknownObjectProperties(why) {
   return [{
     name: "Unknown properties",
     desc: {
       value: why,
       enumerable: true,
     },
   }];
 }
 
+function getObjectData(id) {
+  const object = gPausedObjects.getObject(id);
+  if (object instanceof Debugger.Object) {
+    const rv = {
+      id,
+      kind: "Object",
+      callable: object.callable,
+      isBoundFunction: object.isBoundFunction,
+      isArrowFunction: object.isArrowFunction,
+      isGeneratorFunction: object.isGeneratorFunction,
+      isAsyncFunction: object.isAsyncFunction,
+      proto: getObjectId(object.proto),
+      class: object.class,
+      name: object.name,
+      displayName: object.displayName,
+      parameterNames: object.parameterNames,
+      script: gScripts.getId(object.script),
+      environment: getObjectId(object.environment),
+      isProxy: object.isProxy,
+      isExtensible: object.isExtensible(),
+      isSealed: object.isSealed(),
+      isFrozen: object.isFrozen(),
+    };
+    if (rv.isBoundFunction) {
+      rv.boundTargetFunction = getObjectId(object.boundTargetFunction);
+      rv.boundThis = convertValue(object.boundThis);
+      rv.boundArguments = getObjectId(makeDebuggeeValue(object.boundArguments));
+    }
+    if (rv.isProxy) {
+      rv.proxyUnwrapped = convertValue(object.unwrap());
+      rv.proxyTarget = convertValue(object.proxyTarget);
+      rv.proxyHandler = convertValue(object.proxyHandler);
+    }
+    return rv;
+  }
+  if (object instanceof Debugger.Environment) {
+    return {
+      id,
+      kind: "Environment",
+      type: object.type,
+      parent: getObjectId(object.parent),
+      object: object.type == "declarative" ? 0 : getObjectId(object.object),
+      callee: getObjectId(object.callee),
+      optimizedOut: object.optimizedOut,
+    };
+  }
+  throw new Error("Unknown object kind");
+}
+
 function getObjectProperties(object) {
   let names;
   try {
     names = object.getOwnPropertyNames();
   } catch (e) {
     return unknownObjectProperties(e.toString());
   }
 
-  return names.map(name => {
+  const rv = Object.create(null);
+  names.forEach(name => {
     let desc;
     try {
       desc = object.getOwnPropertyDescriptor(name);
     } catch (e) {
-      return { name, desc: { value: "Unknown: " + e, enumerable: true } };
+      desc = { name, desc: { value: "Unknown: " + e, enumerable: true } };
     }
     if ("value" in desc) {
       desc.value = convertValue(desc.value);
     }
     if ("get" in desc) {
       desc.get = getObjectId(desc.get);
     }
     if ("set" in desc) {
       desc.set = getObjectId(desc.set);
     }
-    return { name, desc };
+    rv[name] = desc;
   });
+  return rv;
+}
+
+function getEnvironmentNames(env) {
+  try {
+    const names = env.names();
+
+    return names.map(name => {
+      return { name, value: convertValue(env.getVariable(name)) };
+    });
+  } catch (e) {
+    return [{name: "Unknown names",
+             value: "Exception thrown in getEnvironmentNames" }];
+  }
 }
 
 function getWindow() {
   // Hopefully there is exactly one window in this enumerator.
   for (const window of Services.ww.getWindowEnumerator()) {
     return window;
   }
   return null;
 }
 
+// Maximum number of properties the server is interested in when previewing an
+// object.
+const OBJECT_PREVIEW_MAX_ITEMS = 10;
+
+// When the replaying process pauses, the server needs to inspect a lot of state
+// around frames, objects, etc. in order to fill in all the information the
+// client needs to update the UI for the pause location. Done naively, this
+// inspection requires a lot of back and forth with the replaying process to
+// get all this data. This is bad for performance, and especially so if the
+// replaying process is on a different machine from the server. Instead, the
+// debugger running in the server can request a pause data packet which includes
+// everything the server will need.
+//
+// This should avoid overapproximation, so that we can quickly send pause data
+// across a network connection, and especially should not underapproximate
+// as the server will end up needing to make more requests before the client can
+// finish pausing.
+function getPauseData() {
+  const numFrames = countScriptFrames();
+  if (!numFrames) {
+    return {};
+  }
+
+  const rv = {
+    frames: [],
+    scripts: {},
+    offsetMetadata: [],
+    objects: {},
+    environments: {},
+  };
+
+  function addValue(value, includeProperties) {
+    if (value && typeof value == "object" && value.object) {
+      addObject(value.object, includeProperties);
+    }
+  }
+
+  function addObject(id, includeProperties) {
+    if (!id) {
+      return;
+    }
+
+    // If includeProperties is set then previewing the object requires knowledge
+    // of its enumerable properties.
+    const needObject = !rv.objects[id];
+    const needProperties =
+      includeProperties &&
+      (needObject || !rv.objects[id].preview.enumerableOwnProperties);
+
+    if (!needObject && !needProperties) {
+      return;
+    }
+
+    const object = gPausedObjects.getObject(id);
+    assert(object instanceof Debugger.Object);
+
+    const properties = getObjectProperties(object);
+    const propertyEntries = Object.entries(properties);
+
+    if (needObject) {
+      rv.objects[id] = {
+        data: getObjectData(id),
+        preview: {
+          ownPropertyNamesCount: propertyEntries.length,
+        },
+      };
+
+      const preview = rv.objects[id].preview;
+
+      // Add some properties (if present) which the server might ask for
+      // even when it isn't interested in the rest of the properties.
+      if (properties.length) {
+        preview.lengthProperty = properties.length;
+      }
+      if (properties.displayName) {
+        preview.displayNameProperty = properties.displayName;
+      }
+    }
+
+    if (needProperties) {
+      const preview = rv.objects[id].preview;
+
+      // The server is only interested in enumerable properties, and at most
+      // OBJECT_PREVIEW_MAX_ITEMS of them. Limiting the properties we send to
+      // only those the server needs avoids having to send the contents of huge
+      // objects like Windows, most of which will not be used.
+      const enumerableOwnProperties = Object.create(null);
+      let enumerablePropertyCount = 0;
+      for (const [ name, desc ] of propertyEntries) {
+        if (desc.enumerable) {
+          enumerableOwnProperties[name] = desc;
+          addPropertyDescriptor(desc, false);
+          if (++enumerablePropertyCount == OBJECT_PREVIEW_MAX_ITEMS) {
+            break;
+          }
+        }
+      }
+      preview.enumerableOwnProperties = enumerableOwnProperties;
+    }
+  }
+
+  function addPropertyDescriptor(desc, includeProperties) {
+    if (desc.value) {
+      addValue(desc.value, includeProperties);
+    }
+    if (desc.get) {
+      addObject(desc.get, includeProperties);
+    }
+    if (desc.set) {
+      addObject(desc.set, includeProperties);
+    }
+  }
+
+  function addEnvironment(id) {
+    if (!id || rv.environments[id]) {
+      return;
+    }
+
+    const env = gPausedObjects.getObject(id);
+    assert(env instanceof Debugger.Environment);
+
+    const data = getObjectData(id);
+    const names = getEnvironmentNames(env);
+    rv.environments[id] = { data, names };
+
+    addEnvironment(data.parent);
+  }
+
+  // eslint-disable-next-line no-shadow
+  function addScript(id) {
+    if (!rv.scripts[id]) {
+      rv.scripts[id] = getScriptData(id);
+    }
+  }
+
+  for (let i = 0; i < numFrames; i++) {
+    const frame = getFrameData(i);
+    const script = gScripts.getObject(frame.script);
+    rv.frames.push(frame);
+    rv.offsetMetadata.push({
+      scriptId: frame.script,
+      offset: frame.offset,
+      metadata: script.getOffsetMetadata(frame.offset),
+    });
+    addScript(frame.script);
+    addValue(frame.this, true);
+    if (frame.arguments) {
+      for (const arg of frame.arguments) {
+        addValue(arg, true);
+      }
+    }
+    addObject(frame.callee, false);
+    addEnvironment(frame.environment, true);
+  }
+
+  return rv;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // Handlers
 ///////////////////////////////////////////////////////////////////////////////
 
 const gRequestHandlers = {
 
   repaint() {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
@@ -728,80 +975,28 @@ const gRequestHandlers = {
     return sources;
   },
 
   getSource(request) {
     return getSourceData(request.id);
   },
 
   getObject(request) {
-    const object = gPausedObjects.getObject(request.id);
-    if (object instanceof Debugger.Object) {
-      const rv = {
-        id: request.id,
-        kind: "Object",
-        callable: object.callable,
-        isBoundFunction: object.isBoundFunction,
-        isArrowFunction: object.isArrowFunction,
-        isGeneratorFunction: object.isGeneratorFunction,
-        isAsyncFunction: object.isAsyncFunction,
-        proto: getObjectId(object.proto),
-        class: object.class,
-        name: object.name,
-        displayName: object.displayName,
-        parameterNames: object.parameterNames,
-        script: gScripts.getId(object.script),
-        environment: getObjectId(object.environment),
-        isProxy: object.isProxy,
-        isExtensible: object.isExtensible(),
-        isSealed: object.isSealed(),
-        isFrozen: object.isFrozen(),
-      };
-      if (rv.isBoundFunction) {
-        rv.boundTargetFunction = getObjectId(object.boundTargetFunction);
-        rv.boundThis = convertValue(object.boundThis);
-        rv.boundArguments = getObjectId(makeDebuggeeValue(object.boundArguments));
-      }
-      return rv;
-    }
-    if (object instanceof Debugger.Environment) {
-      return {
-        id: request.id,
-        kind: "Environment",
-        type: object.type,
-        parent: getObjectId(object.parent),
-        object: object.type == "declarative" ? 0 : getObjectId(object.object),
-        callee: getObjectId(object.callee),
-        optimizedOut: object.optimizedOut,
-      };
-    }
-    throw new Error("Unknown object kind");
+    return getObjectData(request.id);
   },
 
   getObjectProperties(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return unknownObjectProperties("Recording divergence in getObjectProperties");
     }
 
     const object = gPausedObjects.getObject(request.id);
     return getObjectProperties(object);
   },
 
-  objectProxyData(request) {
-    if (!RecordReplayControl.maybeDivergeFromRecording()) {
-      return { exception: "Recording divergence in unwrapObject" };
-    }
-    const obj = gPausedObjects.getObject(request.id);
-    return {
-      unwrapped: convertValue(obj.unwrap()),
-      target: convertValue(obj.proxyTarget),
-      handler: convertValue(obj.proxyHandler),
-    };
-  },
-
   objectApply(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return { throw: "Recording divergence in objectApply" };
     }
     const obj = gPausedObjects.getObject(request.id);
     const thisv = convertValueFromParent(request.thisv);
     const args = request.args.map(v => convertValueFromParent(v));
     const rv = obj.apply(thisv, args);
@@ -809,61 +1004,40 @@ const gRequestHandlers = {
   },
 
   getEnvironmentNames(request) {
     if (!RecordReplayControl.maybeDivergeFromRecording()) {
       return [{name: "Unknown names",
                value: "Recording divergence in getEnvironmentNames" }];
     }
 
-    try {
-      const env = gPausedObjects.getObject(request.id);
-      const names = env.names();
-
-      return names.map(name => {
-        return { name, value: convertValue(env.getVariable(name)) };
-      });
-    } catch (e) {
-      return [{name: "Unknown names",
-               value: "Exception thrown in getEnvironmentNames" }];
-    }
+    const env = gPausedObjects.getObject(request.id);
+    return getEnvironmentNames(env);
   },
 
   getFrame(request) {
     if (request.index == -1 /* NewestFrameIndex */) {
       const numFrames = countScriptFrames();
+
       if (!numFrames) {
         // Return an empty object when there are no frames.
         return {};
       }
       request.index = numFrames - 1;
     }
 
-    const frame = scriptFrameForIndex(request.index);
+    return getFrameData(request.index);
+  },
 
-    let _arguments = null;
-    if (frame.arguments) {
-      _arguments = [];
-      for (let i = 0; i < frame.arguments.length; i++) {
-        _arguments.push(convertValue(frame.arguments[i]));
-      }
+  pauseData(request) {
+    if (!RecordReplayControl.maybeDivergeFromRecording()) {
+      return { error: "Recording divergence in pauseData" };
     }
 
-    return {
-      index: request.index,
-      type: frame.type,
-      callee: getObjectId(frame.callee),
-      environment: getObjectId(frame.environment),
-      generator: frame.generator,
-      constructing: frame.constructing,
-      this: convertValue(frame.this),
-      script: gScripts.getId(frame.script),
-      offset: frame.offset,
-      arguments: _arguments,
-    };
+    return getPauseData();
   },
 
   getLineOffsets: forwardToScript("getLineOffsets"),
   getOffsetLocation: forwardToScript("getOffsetLocation"),
   getSuccessorOffsets: forwardToScript("getSuccessorOffsets"),
   getPredecessorOffsets: forwardToScript("getPredecessorOffsets"),
   getAllColumnOffsets: forwardToScript("getAllColumnOffsets"),
   getOffsetMetadata: forwardToScript("getOffsetMetadata"),
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -8,17 +8,17 @@
 
 const Services = require("Services");
 const { Cr, Ci } = require("chrome");
 const { ActorPool } = require("devtools/server/actors/common");
 const { createValueGrip } = require("devtools/server/actors/object/utils");
 const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert, dumpn } = DevToolsUtils;
-const { threadSpec } = require("devtools/shared/specs/script");
+const { threadSpec } = require("devtools/shared/specs/thread");
 const {
   getAvailableEventBreakpoints,
 } = require("devtools/server/actors/utils/event-breakpoints");
 
 loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
 loader.lazyRequireGetter(this, "BreakpointActorMap", "devtools/server/actors/utils/breakpoint-actor-map", true);
 loader.lazyRequireGetter(this, "PauseScopedObjectActor", "devtools/server/actors/pause-scoped", true);
 loader.lazyRequireGetter(this, "EventLoopStack", "devtools/server/actors/utils/event-loop", true);
--- a/devtools/server/tests/browser/browser_accessibility_walker_audit.js
+++ b/devtools/server/tests/browser/browser_accessibility_walker_audit.js
@@ -54,30 +54,59 @@ add_task(async function() {
         "value": 4.00,
         "color": [255, 0, 0, 1],
         "backgroundColor": [255, 255, 255, 1],
         "isLargeText": false,
         "score": "fail",
       },
     },
   }];
+  const total = accessibles.length;
+  const expectedProgress = [
+    { total, percentage: 20 },
+    { total, percentage: 40 },
+    { total, percentage: 60 },
+    { total, percentage: 80 },
+    { total, percentage: 100},
+  ];
 
   function findAccessible(name, role) {
     return accessibles.find(accessible =>
       accessible.name === name && accessible.role === role);
   }
 
   const a11yWalker = await accessibility.getWalker();
   ok(a11yWalker, "The AccessibleWalkerFront was returned");
   await accessibility.enable();
 
   info("Checking AccessibleWalker audit functionality");
-  const auditEvent = a11yWalker.once("audit-event");
-  a11yWalker.startAudit();
-  const { ancestries } = await auditEvent;
+  const ancestries = await new Promise((resolve, reject) => {
+    const auditEventHandler = ({ type, ancestries: response, progress }) => {
+      switch (type) {
+        case "error":
+          a11yWalker.off("audit-event", auditEventHandler);
+          reject();
+          break;
+        case "completed":
+          a11yWalker.off("audit-event", auditEventHandler);
+          resolve(response);
+          is(expectedProgress.length, 0, "All progress events fired");
+          break;
+        case "progress":
+          SimpleTest.isDeeply(progress, expectedProgress.shift(),
+                              "Progress data is correct");
+          break;
+        default:
+          break;
+      }
+    };
+
+    a11yWalker.on("audit-event", auditEventHandler);
+    a11yWalker.startAudit();
+  });
 
   is(ancestries.length, 2, "The size of ancestries is correct");
   for (const ancestry of ancestries) {
     for (const { accessible, children } of ancestry) {
       checkA11yFront(accessible,
                      findAccessible(accessibles.name, accessibles.role));
       for (const child of children) {
         checkA11yFront(child,
--- a/devtools/shared/specs/accessibility.js
+++ b/devtools/shared/specs/accessibility.js
@@ -16,25 +16,26 @@ types.addActorType("accessible");
 types.addDictType("accessibleWithChildren", {
   // Accessible
   accessible: "accessible",
   // Accessible's children
   children: "array:accessible",
 });
 
 /**
- * Data passed via "audit-event" to the client. It may include a list of
+ * Data passed via "audit-event" to the client. It may include type, a list of
  * ancestries for accessible actors that have failing accessibility checks or
- * an error flag.
+ * a progress information.
  */
 types.addDictType("auditEventData", {
+  type: "string",
   // List of ancestries (array:accessibleWithChildren)
-  ancestries: "array:array:accessibleWithChildren",
-  // True if the audit failed.
-  error: "boolean",
+  ancestries: "nullable:array:array:accessibleWithChildren",
+  // Audit progress information
+  progress: "nullable:json",
 });
 
 /**
  * Accessible relation object described by its type that also includes relation targets.
  */
 types.addDictType("accessibleRelation", {
   // Accessible relation type
   type: "string",
--- a/devtools/shared/specs/index.js
+++ b/devtools/shared/specs/index.js
@@ -166,22 +166,16 @@ const Types = exports.__TypesForTests = 
     spec: "devtools/shared/specs/reflow",
     front: "devtools/shared/fronts/reflow",
   },
   {
     types: ["screenshot"],
     spec: "devtools/shared/specs/screenshot",
     front: "devtools/shared/fronts/screenshot",
   },
-  /* Script and source have old fashion client and no front */
-  {
-    types: ["context"],
-    spec: "devtools/shared/specs/script",
-    front: null,
-  },
   {
     types: ["source"],
     spec: "devtools/shared/specs/source",
     front: "devtools/shared/fronts/source",
   },
   {
     types: ["cookies", "localStorage", "sessionStorage", "Cache", "indexedDB", "storage"],
     spec: "devtools/shared/specs/storage",
@@ -253,16 +247,22 @@ const Types = exports.__TypesForTests = 
     spec: "devtools/shared/specs/targets/webextension",
     front: null,
   },
   {
     types: ["workerTarget"],
     spec: "devtools/shared/specs/targets/worker",
     front: "devtools/shared/fronts/targets/worker",
   },
+  /* Thread has an old fashion client and no front */
+  {
+    types: ["context"],
+    spec: "devtools/shared/specs/thread",
+    front: null,
+  },
   {
     types: ["console"],
     spec: "devtools/shared/specs/webconsole",
     front: "devtools/shared/fronts/webconsole",
   },
   {
     types: ["pushSubscription"],
     spec: "devtools/shared/specs/worker/push-subscription",
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -36,19 +36,19 @@ DevToolsModules(
     'performance-recording.js',
     'performance.js',
     'preference.js',
     'promises.js',
     'property-iterator.js',
     'reflow.js',
     'root.js',
     'screenshot.js',
-    'script.js',
     'source.js',
     'storage.js',
     'string.js',
     'styles.js',
     'stylesheets.js',
     'symbol-iterator.js',
     'symbol.js',
+    'thread.js',
     'timeline.js',
     'webconsole.js',
 )
rename from devtools/shared/specs/script.js
rename to devtools/shared/specs/thread.js
--- a/devtools/shared/specs/script.js
+++ b/devtools/shared/specs/thread.js
@@ -1,28 +1,94 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const {Arg, RetVal, generateActorSpec, types} = require("devtools/shared/protocol");
+const {Arg, Option, RetVal, generateActorSpec, types} = require("devtools/shared/protocol");
 
 types.addDictType("available-breakpoint-group", {
   name: "string",
   events: "array:available-breakpoint-event",
 });
 types.addDictType("available-breakpoint-event", {
   id: "string",
   name: "string",
 });
 
 const threadSpec = generateActorSpec({
   typeName: "context",
 
+  events: {
+    newSource: {
+      source: Option(0, "source"),
+    },
+    progress: {
+      recording: Option(0, "json"),
+      executionPoint: Option(0, "json"),
+    },
+  },
+
   methods: {
+    attach: {
+      request: {
+        options: Arg(0, "json"),
+      },
+      response: RetVal("nullable:json"),
+    },
+    detach: {
+      response: {},
+    },
+    reconfigure: {
+      request: {
+        options: Arg(0, "json"),
+      },
+      response: {},
+    },
+    resume: {
+      request: {
+        resumeLimit: Arg(0, "nullable:json"),
+        rewind: Arg(1, "boolean"),
+      },
+      response: RetVal("nullable:json"),
+    },
+    frames: {
+      request: {
+        start: Arg(0, "number"),
+        count: Arg(1, "number"),
+      },
+      response: RetVal("json"),
+    },
+    interrupt: {
+      request: {
+        when: Arg(0, "json"),
+      },
+      response: RetVal("array:json"),
+    },
+    sources: {
+      response: RetVal("array:json"),
+    },
+    skipBreakpoints: {
+      request: {
+        skip: Arg(0, "json"),
+      },
+      response: {
+        skip: Arg(0, "json"),
+      },
+    },
+    threadGrips: {
+      request: {
+        actors: Arg(0, "array:string"),
+      },
+      response: RetVal("json"),
+    },
+    dumpThread: {
+      request: {},
+      response: RetVal("json"),
+    },
     setBreakpoint: {
       request: {
         location: Arg(0, "json"),
         options: Arg(1, "json"),
       },
     },
     removeBreakpoint: {
       request: {
--- a/docshell/base/nsIDocShellTreeItem.idl
+++ b/docshell/base/nsIDocShellTreeItem.idl
@@ -13,17 +13,17 @@ interface nsPIDOMWindowOuter;
 webidl Document;
 
 /**
  * The nsIDocShellTreeItem supplies the methods that are required of any item
  * that wishes to be able to live within the docshell tree either as a middle
  * node or a leaf. 
  */
 
-[scriptable, uuid(9b7c586f-9214-480c-a2c4-49b526fff1a6)]
+[scriptable, builtinclass, uuid(9b7c586f-9214-480c-a2c4-49b526fff1a6)]
 interface nsIDocShellTreeItem : nsISupports
 {
 	/*
 	name of the DocShellTreeItem
 	*/
 	attribute AString name;
 
         /**
--- a/docshell/test/mochitest/mochitest.ini
+++ b/docshell/test/mochitest/mochitest.ini
@@ -68,16 +68,17 @@ skip-if = true
 [test_bug511449.html]
 skip-if = toolkit != "cocoa" || headless # Headless: bug 1410525
 support-files = file_bug511449.html
 [test_bug529119-1.html]
 [test_bug529119-2.html]
 [test_bug530396.html]
 support-files = bug530396-noref.sjs bug530396-subframe.html
 [test_bug540462.html]
+skip-if = toolkit == 'android' && debug && !is_fennec
 [test_bug551225.html]
 [test_bug570341.html]
 skip-if = (verify && !debug && (os == 'win'))
 [test_bug580069.html]
 skip-if = (verify && !debug && (os == 'win'))
 [test_bug590573.html]
 [test_bug598895.html]
 skip-if = toolkit == 'android'
--- a/dom/base/ProcessSelector.jsm
+++ b/dom/base/ProcessSelector.jsm
@@ -6,18 +6,18 @@
 // ones.
 function RandomSelector() {
 }
 
 RandomSelector.prototype = {
   classID:          Components.ID("{c616fcfd-9737-41f1-aa74-cee72a38f91b}"),
   QueryInterface:   ChromeUtils.generateQI([Ci.nsIContentProcessProvider]),
 
-  provideProcess(aType, aOpener, aProcesses, aCount, aMaxCount) {
-    if (aCount < aMaxCount) {
+  provideProcess(aType, aOpener, aProcesses, aMaxCount) {
+    if (aProcesses.length < aMaxCount) {
       return Ci.nsIContentProcessProvider.NEW_PROCESS;
     }
 
     let startIdx = Math.floor(Math.random() * aMaxCount);
     let curIdx = startIdx;
 
     do {
       if (aProcesses[curIdx].opener === aOpener) {
@@ -35,26 +35,26 @@ RandomSelector.prototype = {
 // ones that host the least number of tabs.
 function MinTabSelector() {
 }
 
 MinTabSelector.prototype = {
   classID:          Components.ID("{2dc08eaf-6eef-4394-b1df-a3a927c1290b}"),
   QueryInterface:   ChromeUtils.generateQI([Ci.nsIContentProcessProvider]),
 
-  provideProcess(aType, aOpener, aProcesses, aCount, aMaxCount) {
-    if (aCount < aMaxCount) {
+  provideProcess(aType, aOpener, aProcesses, aMaxCount) {
+    if (aProcesses.length < aMaxCount) {
       return Ci.nsIContentProcessProvider.NEW_PROCESS;
     }
 
     let min = Number.MAX_VALUE;
     let candidate = Ci.nsIContentProcessProvider.NEW_PROCESS;
 
     // Note, that at this point aMaxCount is in the valid range and
-    // the reason for not using aCount here is because if we keep
+    // the reason for not using aProcesses.length here is because if we keep
     // processes alive for testing but want a test to use only single
     // content process we can just keep relying on dom.ipc.processCount = 1
     // this way.
     for (let i = 0; i < aMaxCount; i++) {
       let process = aProcesses[i];
       let tabCount = process.tabCount;
       if (process.opener === aOpener && tabCount < min) {
         min = tabCount;
--- a/dom/base/TabGroup.cpp
+++ b/dom/base/TabGroup.cpp
@@ -75,18 +75,24 @@ void TabGroup::EnsureThrottledEventQueue
   if (mThrottledQueuesInitialized) {
     return;
   }
 
   mThrottledQueuesInitialized = true;
 
   for (size_t i = 0; i < size_t(TaskCategory::Count); i++) {
     TaskCategory category = static_cast<TaskCategory>(i);
-    if (category == TaskCategory::Worker || category == TaskCategory::Timer) {
-      mEventTargets[i] = ThrottledEventQueue::Create(mEventTargets[i]);
+    if (category == TaskCategory::Worker) {
+      mEventTargets[i] =
+          ThrottledEventQueue::Create(mEventTargets[i],
+                                      "TabGroup worker queue");
+    } else if (category == TaskCategory::Timer) {
+      mEventTargets[i] =
+          ThrottledEventQueue::Create(mEventTargets[i],
+                                      "TabGroup timer queue");
     }
   }
 }
 
 /* static */
 TabGroup* TabGroup::GetChromeTabGroup() {
   if (!sChromeTabGroup) {
     sChromeTabGroup = new TabGroup(true /* chrome tab group */);
--- a/dom/base/nsIObjectLoadingContent.idl
+++ b/dom/base/nsIObjectLoadingContent.idl
@@ -18,17 +18,17 @@ class nsNPAPIPluginInstance;
 
 /**
  * This interface represents a content node that loads objects.
  *
  * Please make sure to update the MozObjectLoadingContent WebIDL
  * interface to mirror this interface when changing it.
  */
 
-[scriptable, uuid(2eb3195e-3eea-4083-bb1d-d2d70fa35ccb)]
+[scriptable, builtinclass, uuid(2eb3195e-3eea-4083-bb1d-d2d70fa35ccb)]
 interface nsIObjectLoadingContent : nsISupports
 {
   /**
    * See notes in nsObjectLoadingContent.h
    */
   const unsigned long TYPE_LOADING     = 0;
   const unsigned long TYPE_IMAGE       = 1;
   const unsigned long TYPE_PLUGIN      = 2;
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -840,10 +840,11 @@ tags = audiochannel
 [test_window_extensible.html]
 [test_window_indexing.html]
 [test_window_keys.html]
 [test_window_named_frame_enumeration.html]
 [test_window_own_props.html]
 [test_window_proto.html]
 [test_writable-replaceable.html]
 [test_x-frame-options.html]
+skip-if = toolkit == 'android' && debug && !is_fennec
 [test_youtube_flash_embed.html]
 # Please keep alphabetical order.
--- a/dom/bindings/BindingUtils.cpp
+++ b/dom/bindings/BindingUtils.cpp
@@ -1138,16 +1138,27 @@ static int CompareIdsAtIndices(const voi
   const uint16_t index2 = *static_cast<const uint16_t*>(aElement2);
   const PropertyInfo* infos = static_cast<PropertyInfo*>(aClosure);
 
   MOZ_ASSERT(JSID_BITS(infos[index1].Id()) != JSID_BITS(infos[index2].Id()));
 
   return JSID_BITS(infos[index1].Id()) < JSID_BITS(infos[index2].Id()) ? -1 : 1;
 }
 
+// {JSPropertySpec,JSFunctionSpec} use {JSPropertySpec,JSFunctionSpec}::Name
+// and ConstantSpec uses `const char*` for name field.
+static inline JSPropertySpec::Name ToPropertySpecName(
+    JSPropertySpec::Name name) {
+  return name;
+}
+
+static inline JSPropertySpec::Name ToPropertySpecName(const char* name) {
+  return JSPropertySpec::Name(name);
+}
+
 template <typename SpecT>
 static bool InitIdsInternal(JSContext* cx, const Prefable<SpecT>* pref,
                             PropertyInfo* infos, PropertyType type) {
   MOZ_ASSERT(pref);
   MOZ_ASSERT(pref->specs);
 
   // Index of the Prefable that contains the id for the current PropertyInfo.
   uint32_t prefIndex = 0;
@@ -1156,17 +1167,18 @@ static bool InitIdsInternal(JSContext* c
     // We ignore whether the set of ids is enabled and just intern all the IDs,
     // because this is only done once per application runtime.
     const SpecT* spec = pref->specs;
     // Index of the property/function/constant spec for our current PropertyInfo
     // in the "specs" array of the relevant Prefable.
     uint32_t specIndex = 0;
     do {
       jsid id;
-      if (!JS::PropertySpecNameToPermanentId(cx, spec->name, &id)) {
+      if (!JS::PropertySpecNameToPermanentId(cx, ToPropertySpecName(spec->name),
+                                             &id)) {
         return false;
       }
       infos->SetId(id);
       infos->type = type;
       infos->prefIndex = prefIndex;
       infos->specIndex = specIndex++;
       ++infos;
     } while ((++spec)->name);
--- a/dom/events/EventStateManager.h
+++ b/dom/events/EventStateManager.h
@@ -1278,36 +1278,31 @@ class EventStateManager : public nsSuppo
                                                 nsIContent* aElement);
   static void sClickHoldCallback(nsITimer* aTimer, void* aESM);
 };
 
 /**
  * This class is used while processing real user input. During this time, popups
  * are allowed. For mousedown events, mouse capturing is also permitted.
  */
-class AutoHandlingUserInputStatePusher {
+class MOZ_RAII AutoHandlingUserInputStatePusher final {
  public:
   AutoHandlingUserInputStatePusher(bool aIsHandlingUserInput,
                                    WidgetEvent* aEvent,
                                    dom::Document* aDocument);
   ~AutoHandlingUserInputStatePusher();
 
  protected:
   RefPtr<dom::Document> mMouseButtonEventHandlingDocument;
   EventMessage mMessage;
   bool mIsHandlingUserInput;
 
   bool NeedsToResetFocusManagerMouseButtonHandlingState() const {
     return mMessage == eMouseDown || mMessage == eMouseUp;
   }
-
- private:
-  // Hide so that this class can only be stack-allocated
-  static void* operator new(size_t /*size*/) CPP_THROW_NEW { return nullptr; }
-  static void operator delete(void* /*memory*/) {}
 };
 
 }  // namespace mozilla
 
 // Click and double-click events need to be handled even for content that
 // has no frame. This is required for Web compatibility.
 #define NS_EVENT_NEEDS_FRAME(event)               \
   (!(event)->HasPluginActivationEventMessage() && \
--- a/dom/fetch/Response.cpp
+++ b/dom/fetch/Response.cpp
@@ -162,16 +162,21 @@ already_AddRefed<Response> Response::Red
 
 /*static*/
 already_AddRefed<Response> Response::Constructor(
     const GlobalObject& aGlobal,
     const Optional<Nullable<fetch::ResponseBodyInit>>& aBody,
     const ResponseInit& aInit, ErrorResult& aRv) {
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
 
+  if (NS_WARN_IF(!global)) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return nullptr;
+  }
+
   if (aInit.mStatus < 200 || aInit.mStatus > 599) {
     aRv.ThrowRangeError<MSG_INVALID_RESPONSE_STATUSCODE_ERROR>();
     return nullptr;
   }
 
   // Check if the status text contains illegal characters
   nsACString::const_iterator start, end;
   aInit.mStatusText.BeginReading(start);
@@ -204,20 +209,32 @@ already_AddRefed<Response> Response::Con
 
       principalInfo.reset(new mozilla::ipc::PrincipalInfo());
       nsresult rv =
           PrincipalToPrincipalInfo(doc->NodePrincipal(), principalInfo.get());
       if (NS_WARN_IF(NS_FAILED(rv))) {
         aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
         return nullptr;
       }
-    } else {
+
+      internalResponse->InitChannelInfo(info);
+    } else if (nsContentUtils::IsSystemPrincipal(global->PrincipalOrNull())) {
       info.InitFromChromeGlobal(global);
+
+      internalResponse->InitChannelInfo(info);
     }
-    internalResponse->InitChannelInfo(info);
+
+    /**
+     * The channel info is left uninitialized if neither the above `if` nor
+     * `else if` statements are executed; this could be because we're in a
+     * WebExtensions content script, where the global (i.e. `global`) is a
+     * wrapper, and the principal is an expanded principal. In this case,
+     * as far as I can tell, there's no way to get the security info, but we'd
+     * like the `Response` to be successfully constructed.
+     */
   } else {
     WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
     MOZ_ASSERT(worker);
     internalResponse->InitChannelInfo(worker->GetChannelInfo());
     principalInfo =
         MakeUnique<mozilla::ipc::PrincipalInfo>(worker->GetPrincipalInfo());
   }
 
--- a/dom/fetch/moz.build
+++ b/dom/fetch/moz.build
@@ -55,12 +55,13 @@ LOCAL_INCLUDES += [
     '/netwerk/base',
     # For nsDataHandler.h
     '/netwerk/protocol/data',
     # For HttpBaseChannel.h
     '/netwerk/protocol/http',
 ]
 
 BROWSER_CHROME_MANIFESTS += [ 'tests/browser.ini' ]
+MOCHITEST_MANIFESTS += [ 'tests/mochitest.ini' ]
 
 FINAL_LIBRARY = 'xul'
 
 include('/ipc/chromium/chromium-config.mozbuild')
new file mode 100644
--- /dev/null
+++ b/dom/fetch/tests/mochitest.ini
@@ -0,0 +1,1 @@
+[test_ext_response_constructor.html]
new file mode 100644
--- /dev/null
+++ b/dom/fetch/tests/test_ext_response_constructor.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test `Response` constructor in a WebExtension</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+  <script>
+    add_task(async function testResponseConstructor() {
+      function contentScript() {
+        new Response();
+        browser.test.notifyPass("done");
+      }
+
+      const extension = ExtensionTestUtils.loadExtension({
+        manifest: {
+          content_scripts: [
+            {
+              matches: ["<all_urls>"],
+              js: ["content_script.js"],
+            },
+          ],
+        },
+
+        files: {
+          "content_script.js": contentScript,
+        },
+      });
+
+      await extension.startup();
+
+      const win = window.open("https://example.com");
+      await extension.awaitFinish("done");
+      win.close();
+
+      await extension.unload();
+    });
+  </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
--- a/dom/html/HTMLFormElement.cpp
+++ b/dom/html/HTMLFormElement.cpp
@@ -49,16 +49,17 @@
 #include "nsIScriptSecurityManager.h"
 #include "nsNetUtil.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIWebProgress.h"
 #include "nsIDocShell.h"
 #include "nsIPrompt.h"
 #include "nsISecurityUITelemetry.h"
 #include "nsIStringBundle.h"
+#include "nsIProtocolHandler.h"
 
 // radio buttons
 #include "mozilla/dom/HTMLInputElement.h"
 #include "nsIRadioVisitor.h"
 #include "RadioNodeList.h"
 
 #include "nsLayoutUtils.h"
 
@@ -762,35 +763,29 @@ nsresult HTMLFormElement::DoSecureToInse
   if (!principalURI) {
     principalURI = OwnerDoc()->GetDocumentURI();
   }
   bool formIsHTTPS;
   rv = principalURI->SchemeIs("https", &formIsHTTPS);
   if (NS_FAILED(rv)) {
     return rv;
   }
-  bool actionIsHTTPS;
-  rv = aActionURL->SchemeIs("https", &actionIsHTTPS);
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  bool actionIsJS;
-  rv = aActionURL->SchemeIs("javascript", &actionIsJS);
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
 
-  if (!formIsHTTPS || actionIsHTTPS || actionIsJS) {
+  if (!formIsHTTPS) {
     return NS_OK;
   }
 
   if (nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(aActionURL)) {
     return NS_OK;
   }
 
+  if (nsMixedContentBlocker::URISafeToBeLoadedInSecureContext(aActionURL)) {
+    return NS_OK;
+  }
+
   if (nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(aActionURL)) {
     return NS_OK;
   }
 
   nsCOMPtr<nsPIDOMWindowOuter> window = OwnerDoc()->GetWindow();
   if (!window) {
     return NS_ERROR_FAILURE;
   }
--- a/dom/html/test/mochitest.ini
+++ b/dom/html/test/mochitest.ini
@@ -570,16 +570,17 @@ skip-if = toolkit == "android" || toolki
 [test_non-ascii-cookie.html]
 support-files = file_cookiemanager.js
 [test_bug765780.html]
 [test_bug871161.html]
 support-files = file_bug871161-1.html file_bug871161-2.html
 [test_bug1013316.html]
 [test_window_open_close.html]
 tags = openwindow
+skip-if = toolkit == "android" && debug && !is_fennec
 [test_viewport_resize.html]
 [test_image_clone_load.html]
 [test_bug1203668.html]
 [test_bug1166138.html]
 [test_bug1230665.html]
 [test_filepicker_default_directory.html]
 skip-if = toolkit == 'android'
 [test_bug1233598.html]
--- a/dom/indexedDB/test/mochitest.ini
+++ b/dom/indexedDB/test/mochitest.ini
@@ -164,17 +164,19 @@ skip-if = verify
 [test_file_cross_database_copying.html]
 [test_file_delete.html]
 [test_file_os_delete.html]
 [test_file_put_deleted.html]
 [test_file_put_get_object.html]
 [test_file_put_get_values.html]
 [test_file_replace.html]
 [test_file_resurrection_delete.html]
+skip-if = toolkit == 'android' && !is_fennec
 [test_file_resurrection_transaction_abort.html]
+skip-if = toolkit == 'android' && !is_fennec
 [test_file_sharing.html]
 [test_file_transaction_abort.html]
 [test_filehandle_append_read_data.html]
 [test_filehandle_compat.html]
 [test_filehandle_disabled_pref.html]
 [test_filehandle_getFile.html]
 [test_filehandle_iteration.html]
 [test_filehandle_lifetimes.html]
--- a/dom/interfaces/base/nsIBrowserChild.idl
+++ b/dom/interfaces/base/nsIBrowserChild.idl
@@ -8,17 +8,17 @@
 
 interface nsIWebBrowserChrome3;
 
 webidl ContentFrameMessageManager;
 
 native CommandsArray(nsTArray<nsCString>);
 [ref] native CommandsArrayRef(nsTArray<nsCString>);
 
-[scriptable, uuid(1fb79c27-e760-4088-b19c-1ce3673ec24e)]
+[scriptable, builtinclass, uuid(1fb79c27-e760-4088-b19c-1ce3673ec24e)]
 interface nsIBrowserChild : nsISupports
 {
   readonly attribute ContentFrameMessageManager messageManager;
 
   attribute nsIWebBrowserChrome3 webBrowserChrome;
 
   [notxpcom] void sendRequestFocus(in boolean canFocus);
 
--- a/dom/interfaces/base/nsIContentProcess.idl
+++ b/dom/interfaces/base/nsIContentProcess.idl
@@ -46,11 +46,11 @@ interface nsIContentProcessProvider : ns
   const int32_t NEW_PROCESS = -1;
 
   /**
    * Given aAliveProcesses (with an opener aOpener), choose which process of
    * aType to use. Return nsIContentProcessProvider.NEW_PROCESS to ask the
    * caller to create a new content process.
    */
   int32_t provideProcess(in AString aType, in nsIContentProcessInfo aOpener,
-                         [array, size_is(aCount)] in nsIContentProcessInfo aAliveProcesses,
-                         in uint32_t aCount, in uint32_t aMaxCount);
+                         in Array<nsIContentProcessInfo> aAliveProcesses,
+                         in uint32_t aMaxCount);
 };
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -619,21 +619,25 @@ bool ContentChild::Init(MessageLoop* aIO
   // on its own when deciding which backend to use, and when starting under
   // XWayland, it may choose to start with the wayland backend
   // instead of the x11 backend.
   // The DISPLAY environment variable is normally set by the parent process.
   // The MOZ_GDK_DISPLAY environment variable is set from nsAppRunner.cpp
   // when --display is set by the command line.
   if (!gfxPlatform::IsHeadless()) {
     const char* display_name = PR_GetEnv("MOZ_GDK_DISPLAY");
-#  ifndef MOZ_WAYLAND
     if (!display_name) {
-      display_name = PR_GetEnv("DISPLAY");
+      bool waylandDisabled = true;
+#  ifdef MOZ_WAYLAND
+      waylandDisabled = IsWaylandDisabled();
+#  endif
+      if (waylandDisabled) {
+        display_name = PR_GetEnv("DISPLAY");
+      }
     }
-#  endif
     if (display_name) {
       int argc = 3;
       char option_name[] = "--display";
       char* argv[] = {
           // argv0 is unused because g_set_prgname() was called in
           // XRE_InitChildProcess().
           nullptr, option_name, const_cast<char*>(display_name), nullptr};
       char** argvp = argv;
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -837,35 +837,34 @@ already_AddRefed<ContentParent> ContentP
     // We never want to re-use Large-Allocation processes.
     if (contentParents.Length() >= maxContentParents) {
       return GetNewOrUsedBrowserProcess(aFrameElement,
                                         NS_LITERAL_STRING(DEFAULT_REMOTE_TYPE),
                                         aPriority, aOpener);
     }
   } else {
     uint32_t numberOfParents = contentParents.Length();
-    nsTArray<nsIContentProcessInfo*> infos(numberOfParents);
+    nsTArray<RefPtr<nsIContentProcessInfo>> infos(numberOfParents);
     for (auto* cp : contentParents) {
       infos.AppendElement(cp->mScriptableHelper);
     }
 
     if (aPreferUsed && numberOfParents) {
       // For the preloaded browser we don't want to create a new process but
       // reuse an existing one.
       maxContentParents = numberOfParents;
     }
 
     nsCOMPtr<nsIContentProcessProvider> cpp =
         do_GetService("@mozilla.org/ipc/processselector;1");
     nsIContentProcessInfo* openerInfo =
         aOpener ? aOpener->mScriptableHelper.get() : nullptr;
     int32_t index;
-    if (cpp && NS_SUCCEEDED(cpp->ProvideProcess(
-                   aRemoteType, openerInfo, infos.Elements(), infos.Length(),
-                   maxContentParents, &index))) {
+    if (cpp && NS_SUCCEEDED(cpp->ProvideProcess(aRemoteType, openerInfo, infos,
+                                                maxContentParents, &index))) {
       // If the provider returned an existing ContentParent, use that one.
       if (0 <= index && static_cast<uint32_t>(index) <= maxContentParents) {
         RefPtr<ContentParent> retval = contentParents[index];
         return retval.forget();
       }
     } else {
       // If there was a problem with the JS chooser, fall back to a random
       // selection.
--- a/dom/ipc/tests/mochitest.ini
+++ b/dom/ipc/tests/mochitest.ini
@@ -19,11 +19,11 @@ skip-if = !(crashreporter && !e10s && (t
 [test_temporaryfile_stream.html]
 skip-if = !e10s || toolkit == 'android' || (os == "win" && processor == "aarch64") # Bug 1525959, aarch64 due to 1531150
 support-files =
   blob_verify.sjs
   !/dom/canvas/test/captureStream_common.js
 [test_Preallocated.html]
 skip-if = !e10s || toolkit == 'android' # Bug 1525959
 [test_force_oop_iframe.html]
-skip-if = !e10s || (os == "android" && !debug) #Bug 1545178
+skip-if = !e10s || (os == "android" && !debug) || (os == "android" && debug && !is_fennec) #Bug 1545178
 support-files =
   file_dummy.html
--- a/dom/media/test/test_webvtt_seeking.html
+++ b/dom/media/test/test_webvtt_seeking.html
@@ -18,26 +18,26 @@ var CUES_INFO = [
   { id: 1, startTime: 4, endTime: 6, text: "This is cue 1."},
 ];
 
 async function startTest() {
   const video = createVideo();
   const cues = createCues(video);
   await startVideo(video);
 
-  seekVideo(video, cues[0].startTime);
+  await seekVideo(video, cues[0].startTime);
   await waitUntilCueIsShowing(cues[0]);
   checkActiveCueAndInactiveCue(cues[0], cues[1]);
 
-  seekVideo(video, cues[1].startTime);
+  await seekVideo(video, cues[1].startTime);
   await waitUntilCueIsShowing(cues[1]);
   checkActiveCueAndInactiveCue(cues[1], cues[0]);
 
   // seek forward again
-  seekVideo(video, cues[0].startTime);
+  await seekVideo(video, cues[0].startTime);
   await waitUntilCueIsShowing(cues[0]);
   checkActiveCueAndInactiveCue(cues[0], cues[1]);
 
   removeNodeAndSource(video);
   SimpleTest.finish();
 }
 
 SimpleTest.waitForExplicitFinish();
@@ -79,30 +79,32 @@ function createCues(video) {
 
 async function startVideo(video) {
   info(`start play video`);
   const played = video && await video.play().then(() => true, () => false);
   ok(played, "video has started playing");
 }
 
 async function waitUntilCueIsShowing(cue) {
+  info(`wait until cue ${cue.id} shows`);
   // cue has not been showing yet.
   if (!cue.getActive) {
     await once(cue, "enter");
   }
   info(`cue ${cue.id} is showing`);
 }
 
 async function seekVideo(video, time) {
   ok(isInRange(time, CUES_INFO[0].startTime, CUES_INFO[0].endTime) ||
      isInRange(time, CUES_INFO[1].startTime, CUES_INFO[1].endTime),
      `seek target time ${time} is within the correct range`)
   info(`seek video to ${time}`);
   video.currentTime = time;
   await once(video, "seeked");
+  info(`seek succeeded, current time=${video.currentTime}`);
 }
 
 function isInRange(value, lowerBound, higherBound) {
   return lowerBound <= value && value <= higherBound;
 }
 </script>
 </body>
 </html>
--- a/dom/media/webvtt/vtt.jsm
+++ b/dom/media/webvtt/vtt.jsm
@@ -29,16 +29,23 @@ var EXPORTED_SYMBOLS = ["WebVTT"];
 
 const {Services} = ChromeUtils.import('resource://gre/modules/Services.jsm');
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "supportPseudo",
                                       "media.webvtt.pseudo.enabled", false);
 
 (function(global) {
+  var DEBUG_LOG = false;
+
+  function LOG(message) {
+    if (DEBUG_LOG) {
+      dump("[vtt] " + message + "\n");
+    }
+  }
 
   var _objCreate = Object.create || (function() {
     function F() {}
     return function(o) {
       if (arguments.length !== 1) {
         throw new Error('Object.create shim only accepts one parameter.');
       }
       F.prototype = o;
@@ -794,31 +801,41 @@ XPCOMUtils.defineLazyPreferenceGetter(th
     get bottom() {
       return this.top + this.height;
     }
 
     get right() {
       return this.left + this.width;
     }
 
+    // This function is used for debugging, it will return the box's information.
+    getBoxInfoInChars() {
+      return `top=${this.top}, bottom=${this.bottom}, left=${this.left}, ` +
+             `right=${this.right}, width=${this.width}, height=${this.height}`;
+    }
+
     // Move the box along a particular axis. Optionally pass in an amount to move
     // the box. If no amount is passed then the default is the line height of the
     // box.
     move(axis, toMove) {
       switch (axis) {
       case "+x":
+        LOG(`box's left moved from ${this.left} to ${this.left + toMove}`);
         this.left += toMove;
         break;
       case "-x":
+        LOG(`box's left moved from ${this.left} to ${this.left - toMove}`);
         this.left -= toMove;
         break;
       case "+y":
+        LOG(`box's top moved from ${this.top} to ${this.top + toMove}`);
         this.top += toMove;
         break;
       case "-y":
+        LOG(`box's top moved from ${this.top} to ${this.top - toMove}`);
         this.top -= toMove;
         break;
       }
     }
 
     // Check if this box overlaps another box, b2.
     overlaps(b2) {
       return this.left < b2.right &&
@@ -1132,16 +1149,18 @@ XPCOMUtils.defineLazyPreferenceGetter(th
       boxPositions.push(controlBarBox);
     }
 
     // https://w3c.github.io/webvtt/#processing-model 6.1.12.1
     // Create regionNode
     let regionNodeBoxes = {};
     let regionNodeBox;
 
+    LOG(`=== processCues ===`);
+
     for (let i = 0; i < cues.length; i++) {
       cue = cues[i];
       if (cue.region != null) {
        // 6.1.14.1
         styleBox = new RegionCueStyleBox(window, cue);
 
         if (!regionNodeBoxes[cue.region.id]) {
           // create regionNode
@@ -1178,16 +1197,17 @@ XPCOMUtils.defineLazyPreferenceGetter(th
         // result of algorithm doesn't want us to show the cue when we don't
         // have any room for this cue.
         let cueBox = adjustBoxPosition(styleBox, containerBox, controlBarBox, boxPositions);
         if (cueBox) {
           // Remember the computed div so that we don't have to recompute it later
           // if we don't have too.
           cue.displayState = styleBox.div;
           boxPositions.push(cueBox);
+          LOG(`cue ${i}, ` + cueBox.getBoxInfoInChars());
         }
       }
     }
   };
 
   WebVTT.Parser = function(window, decoder) {
     this.window = window;
     this.state = "INITIAL";
--- a/dom/security/nsContentSecurityManager.cpp
+++ b/dom/security/nsContentSecurityManager.cpp
@@ -1074,98 +1074,23 @@ nsContentSecurityManager::IsOriginPotent
   MOZ_ASSERT(NS_IsMainThread());
   NS_ENSURE_ARG_POINTER(aPrincipal);
   NS_ENSURE_ARG_POINTER(aIsTrustWorthy);
 
   if (aPrincipal->IsSystemPrincipal()) {
     *aIsTrustWorthy = true;
     return NS_OK;
   }
-
-  // The following implements:
-  // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
-
   *aIsTrustWorthy = false;
-
   if (aPrincipal->GetIsNullPrincipal()) {
     return NS_OK;
   }
 
   MOZ_ASSERT(aPrincipal->GetIsCodebasePrincipal(),
              "Nobody is expected to call us with an nsIExpandedPrincipal");
 
   nsCOMPtr<nsIURI> uri;
   nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri));
-  if (NS_FAILED(rv)) {
-    return NS_OK;
-  }
-
-  nsAutoCString scheme;
-  rv = uri->GetScheme(scheme);
-  if (NS_FAILED(rv)) {
-    return NS_OK;
-  }
-
-  // Blobs are expected to inherit their principal so we don't expect to have
-  // a codebase principal with scheme 'blob' here.  We can't assert that though
-  // since someone could mess with a non-blob URI to give it that scheme.
-  NS_WARNING_ASSERTION(!scheme.EqualsLiteral("blob"),
-                       "IsOriginPotentiallyTrustworthy ignoring blob scheme");
-
-  // According to the specification, the user agent may choose to extend the
-  // trust to other, vendor-specific URL schemes. We use this for "resource:",
-  // which is technically a substituting protocol handler that is not limited to
-  // local resource mapping, but in practice is never mapped remotely as this
-  // would violate assumptions a lot of code makes.
-  // We use nsIProtocolHandler flags to determine which protocols we consider a
-  // priori authenticated.
-  bool aPrioriAuthenticated = false;
-  if (NS_FAILED(NS_URIChainHasFlags(
-          uri, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY,
-          &aPrioriAuthenticated))) {
-    return NS_ERROR_UNEXPECTED;
-  }
-
-  if (aPrioriAuthenticated) {
-    *aIsTrustWorthy = true;
-    return NS_OK;
-  }
-
-  nsAutoCString host;
-  rv = uri->GetHost(host);
-  if (NS_FAILED(rv)) {
-    return NS_OK;
-  }
-
-  if (host.EqualsLiteral("127.0.0.1") || host.EqualsLiteral("localhost") ||
-      host.EqualsLiteral("::1")) {
-    *aIsTrustWorthy = true;
-    return NS_OK;
-  }
-
-  // If a host is not considered secure according to the default algorithm, then
-  // check to see if it has been whitelisted by the user.  We only apply this
-  // whitelist for network resources, i.e., those with scheme "http" or "ws".
-  // The pref should contain a comma-separated list of hostnames.
-  if (scheme.EqualsLiteral("http") || scheme.EqualsLiteral("ws")) {
-    nsAutoCString whitelist;
-    nsresult rv =
-        Preferences::GetCString("dom.securecontext.whitelist", whitelist);
-    if (NS_SUCCEEDED(rv)) {
-      nsCCharSeparatedTokenizer tokenizer(whitelist, ',');
-      while (tokenizer.hasMoreTokens()) {
-        const nsACString& allowedHost = tokenizer.nextToken();
-        if (host.Equals(allowedHost)) {
-          *aIsTrustWorthy = true;
-          return NS_OK;
-        }
-      }
-    }
-    // Maybe we have a .onion URL. Treat it as whitelisted as well if
-    // `dom.securecontext.whitelist_onions` is `true`.
-    if (nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(uri)) {
-      *aIsTrustWorthy = true;
-      return NS_OK;
-    }
-  }
+  NS_ENSURE_SUCCESS(rv, rv);
+  *aIsTrustWorthy = nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri);
 
   return NS_OK;
 }
--- a/dom/security/nsMixedContentBlocker.cpp
+++ b/dom/security/nsMixedContentBlocker.cpp
@@ -373,17 +373,19 @@ nsMixedContentBlocker::ShouldLoad(nsIURI
 bool nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(nsIURI* aURL) {
   nsAutoCString host;
   nsresult rv = aURL->GetHost(host);
   NS_ENSURE_SUCCESS(rv, false);
 
   // We could also allow 'localhost' (if we can guarantee that it resolves
   // to a loopback address), but Chrome doesn't support it as of writing. For
   // web compat, lets only allow what Chrome allows.
-  return host.EqualsLiteral("127.0.0.1") || host.EqualsLiteral("::1");
+  // see also https://bugzilla.mozilla.org/show_bug.cgi?id=1220810
+  return host.EqualsLiteral("127.0.0.1") || host.EqualsLiteral("::1") ||
+         host.EqualsLiteral("localhost");
 }
 
 /* Maybe we have a .onion URL. Treat it as whitelisted as well if
  * `dom.securecontext.whitelist_onions` is `true`.
  */
 bool nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(nsIURI* aURL) {
   static bool sInited = false;
   static bool sWhiteListOnions = false;
@@ -397,16 +399,88 @@ bool nsMixedContentBlocker::IsPotentiall
   }
 
   nsAutoCString host;
   nsresult rv = aURL->GetHost(host);
   NS_ENSURE_SUCCESS(rv, false);
   return StringEndsWith(host, NS_LITERAL_CSTRING(".onion"));
 }
 
+bool nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(nsIURI* aURI) {
+  // The following implements:
+  // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
+
+  nsAutoCString scheme;
+  nsresult rv = aURI->GetScheme(scheme);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  // Blobs are expected to inherit their principal so we don't expect to have
+  // a codebase principal with scheme 'blob' here.  We can't assert that though
+  // since someone could mess with a non-blob URI to give it that scheme.
+  NS_WARNING_ASSERTION(!scheme.EqualsLiteral("blob"),
+                       "IsOriginPotentiallyTrustworthy ignoring blob scheme");
+
+  // According to the specification, the user agent may choose to extend the
+  // trust to other, vendor-specific URL schemes. We use this for "resource:",
+  // which is technically a substituting protocol handler that is not limited to
+  // local resource mapping, but in practice is never mapped remotely as this
+  // would violate assumptions a lot of code makes.
+  // We use nsIProtocolHandler flags to determine which protocols we consider a
+  // priori authenticated.
+  bool aPrioriAuthenticated = false;
+  if (NS_FAILED(NS_URIChainHasFlags(
+          aURI, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY,
+          &aPrioriAuthenticated))) {
+    return false;
+  }
+
+  if (aPrioriAuthenticated) {
+    return true;
+  }
+
+  nsAutoCString host;
+  rv = aURI->GetHost(host);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  if (IsPotentiallyTrustworthyLoopbackURL(aURI)) {
+    return true;
+  }
+
+  // If a host is not considered secure according to the default algorithm, then
+  // check to see if it has been whitelisted by the user.  We only apply this
+  // whitelist for network resources, i.e., those with scheme "http" or "ws".
+  // The pref should contain a comma-separated list of hostnames.
+
+  if (!scheme.EqualsLiteral("http") && !scheme.EqualsLiteral("ws")) {
+    return false;
+  }
+
+  nsAutoCString whitelist;
+  rv = Preferences::GetCString("dom.securecontext.whitelist", whitelist);
+  if (NS_SUCCEEDED(rv)) {
+    nsCCharSeparatedTokenizer tokenizer(whitelist, ',');
+    while (tokenizer.hasMoreTokens()) {
+      const nsACString& allowedHost = tokenizer.nextToken();
+      if (host.Equals(allowedHost)) {
+        return true;
+      }
+    }
+  }
+  // Maybe we have a .onion URL. Treat it as whitelisted as well if
+  // `dom.securecontext.whitelist_onions` is `true`.
+  if (nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(aURI)) {
+    return true;
+  }
+  return false;
+}
+
 /* Static version of ShouldLoad() that contains all the Mixed Content Blocker
  * logic.  Called from non-static ShouldLoad().
  */
 nsresult nsMixedContentBlocker::ShouldLoad(
     bool aHadInsecureImageRedirect, uint32_t aContentType,
     nsIURI* aContentLocation, nsIURI* aRequestingLocation,
     nsISupports* aRequestingContext, const nsACString& aMimeGuess,
     nsIPrincipal* aRequestPrincipal, int16_t* aDecision) {
@@ -559,59 +633,22 @@ nsresult nsMixedContentBlocker::ShouldLo
   // by the innerMost URL.
   nsCOMPtr<nsIURI> innerContentLocation = NS_GetInnermostURI(aContentLocation);
   if (!innerContentLocation) {
     NS_ERROR("Can't get innerURI from aContentLocation");
     *aDecision = REJECT_REQUEST;
     return NS_OK;
   }
 
-  /* Get the scheme of the sub-document resource to be requested. If it is
-   * a safe to load in an https context then mixed content doesn't apply.
-   *
-   * Check Protocol Flags to determine if scheme is safe to load:
-   * URI_DOES_NOT_RETURN_DATA - e.g.
-   *   "mailto"
-   * URI_IS_LOCAL_RESOURCE - e.g.
-   *   "data",
-   *   "resource",
-   *   "moz-icon"
-   * URI_INHERITS_SECURITY_CONTEXT - e.g.
-   *   "javascript"
-   * URI_IS_POTENTIALLY_TRUSTWORTHY - e.g.
-   *   "https",
-   *   "moz-safe-about"
-   *
-   */
-  bool schemeLocal = false;
-  bool schemeNoReturnData = false;
-  bool schemeInherits = false;
-  bool schemeSecure = false;
-  if (NS_FAILED(NS_URIChainHasFlags(innerContentLocation,
-                                    nsIProtocolHandler::URI_IS_LOCAL_RESOURCE,
-                                    &schemeLocal)) ||
-      NS_FAILED(NS_URIChainHasFlags(
-          innerContentLocation, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA,
-          &schemeNoReturnData)) ||
-      NS_FAILED(
-          NS_URIChainHasFlags(innerContentLocation,
-                              nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT,
-                              &schemeInherits)) ||
-      NS_FAILED(NS_URIChainHasFlags(
-          innerContentLocation,
-          nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY, &schemeSecure))) {
-    *aDecision = REJECT_REQUEST;
-    return NS_ERROR_FAILURE;
-  }
   // TYPE_IMAGE redirects are cached based on the original URI, not the final
   // destination and hence cache hits for images may not have the correct
   // innerContentLocation.  Check if the cached hit went through an http
   // redirect, and if it did, we can't treat this as a secure subresource.
   if (!aHadInsecureImageRedirect &&
-      (schemeLocal || schemeNoReturnData || schemeInherits || schemeSecure)) {
+      URISafeToBeLoadedInSecureContext(innerContentLocation)) {
     *aDecision = ACCEPT;
     return NS_OK;
   }
 
   // Since there are cases where aRequestingLocation and aRequestPrincipal are
   // definitely not the owning document, we try to ignore them by extracting the
   // requestingLocation in the following order:
   // 1) from the aRequestingContext, either extracting
@@ -733,27 +770,17 @@ nsresult nsMixedContentBlocker::ShouldLo
     *aDecision = REJECT_REQUEST;
     return NS_OK;
   }
 
   bool isHttpScheme = false;
   rv = innerContentLocation->SchemeIs("http", &isHttpScheme);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Loopback origins are not considered mixed content even over HTTP. See:
-  // https://w3c.github.io/webappsec-mixed-content/#should-block-fetch
-  if (isHttpScheme &&
-      IsPotentiallyTrustworthyLoopbackURL(innerContentLocation)) {
-    *aDecision = ACCEPT;
-    return NS_OK;
-  }
-
-  // .onion URLs are encrypted and authenticated. Don't treat them as mixed
-  // content if potentially trustworthy (i.e. whitelisted).
-  if (isHttpScheme && IsPotentiallyTrustworthyOnion(innerContentLocation)) {
+  if (isHttpScheme && IsPotentiallyTrustworthyOrigin(innerContentLocation)) {
     *aDecision = ACCEPT;
     return NS_OK;
   }
 
   // The page might have set the CSP directive 'upgrade-insecure-requests'. In
   // such a case allow the http: load to succeed with the promise that the
   // channel will get upgraded to https before fetching any data from the
   // netwerk. Please see: nsHttpChannel::Connect()
@@ -1077,16 +1104,53 @@ nsresult nsMixedContentBlocker::ShouldLo
     // from within ShouldLoad
     nsContentUtils::AddScriptRunner(new nsMixedContentEvent(
         aRequestingContext, classification, rootHasSecureConnection));
     *aDecision = ACCEPT;
     return NS_OK;
   }
 }
 
+bool nsMixedContentBlocker::URISafeToBeLoadedInSecureContext(nsIURI* aURI) {
+  /* Returns a bool if the URI can be loaded as a sub resource safely.
+   *
+   * Check Protocol Flags to determine if scheme is safe to load:
+   * URI_DOES_NOT_RETURN_DATA - e.g.
+   *   "mailto"
+   * URI_IS_LOCAL_RESOURCE - e.g.
+   *   "data",
+   *   "resource",
+   *   "moz-icon"
+   * URI_INHERITS_SECURITY_CONTEXT - e.g.
+   *   "javascript"
+   * URI_IS_POTENTIALLY_TRUSTWORTHY - e.g.
+   *   "https",
+   *   "moz-safe-about"
+   *
+   */
+  bool schemeLocal = false;
+  bool schemeNoReturnData = false;
+  bool schemeInherits = false;
+  bool schemeSecure = false;
+  if (NS_FAILED(NS_URIChainHasFlags(
+          aURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &schemeLocal)) ||
+      NS_FAILED(NS_URIChainHasFlags(
+          aURI, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA,
+          &schemeNoReturnData)) ||
+      NS_FAILED(NS_URIChainHasFlags(
+          aURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT,
+          &schemeInherits)) ||
+      NS_FAILED(NS_URIChainHasFlags(
+          aURI, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY,
+          &schemeSecure))) {
+    return false;
+  }
+  return (schemeLocal || schemeNoReturnData || schemeInherits || schemeSecure);
+}
+
 NS_IMETHODIMP
 nsMixedContentBlocker::ShouldProcess(nsIURI* aContentLocation,
                                      nsILoadInfo* aLoadInfo,
                                      const nsACString& aMimeGuess,
                                      int16_t* aDecision) {
   if (!aContentLocation) {
     // aContentLocation may be null when a plugin is loading without an
     // associated URI resource
--- a/dom/security/nsMixedContentBlocker.h
+++ b/dom/security/nsMixedContentBlocker.h
@@ -46,16 +46,17 @@ class nsMixedContentBlocker : public nsI
   NS_DECL_NSICHANNELEVENTSINK
 
   nsMixedContentBlocker();
 
   // See:
   // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
   static bool IsPotentiallyTrustworthyLoopbackURL(nsIURI* aURL);
   static bool IsPotentiallyTrustworthyOnion(nsIURI* aURL);
+  static bool IsPotentiallyTrustworthyOrigin(nsIURI* aURI);
 
   /* Static version of ShouldLoad() that contains all the Mixed Content Blocker
    * logic.  Called from non-static ShouldLoad().
    * Called directly from imageLib when an insecure redirect exists in a cached
    * image load.
    * @param aHadInsecureImageRedirect
    *        boolean flag indicating that an insecure redirect through http
    *        occured when this image was initially loaded and cached.
@@ -66,16 +67,18 @@ class nsMixedContentBlocker : public nsI
                              nsIURI* aRequestingLocation,
                              nsISupports* aRequestingContext,
                              const nsACString& aMimeGuess,
                              nsIPrincipal* aRequestPrincipal,
                              int16_t* aDecision);
   static void AccumulateMixedContentHSTS(
       nsIURI* aURI, bool aActive, const OriginAttributes& aOriginAttributes);
 
+  static bool URISafeToBeLoadedInSecureContext(nsIURI* aURI);
+
   static bool ShouldUpgradeMixedDisplayContent();
 
   static bool sBlockMixedScript;
   static bool sBlockMixedObjectSubrequest;
   static bool sBlockMixedDisplay;
   static bool sUpgradeMixedDisplay;
 };
 
new file mode 100644
--- /dev/null
+++ b/dom/security/test/csp/file_script_template.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'">
+<template id="a">
+  <script src="file_script_template.js"></script>
+</template>
+</head>
+<body>
+<script>
+  var temp = document.getElementsByTagName("template")[0];
+  var clon = temp.content.cloneNode(true);
+  document.body.appendChild(clon);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/security/test/csp/file_script_template.js
@@ -0,0 +1,1 @@
+// dummy *.js file
--- a/dom/security/test/csp/mochitest.ini
+++ b/dom/security/test/csp/mochitest.ini
@@ -370,8 +370,12 @@ support-files =
   file_nonce_snapshot.sjs
 [test_uir_windowwatcher.html]
 support-files =
   file_windowwatcher_frameA.html
   file_windowwatcher_subframeB.html
   file_windowwatcher_subframeC.html
   file_windowwatcher_subframeD.html
   file_windowwatcher_win_open.html
+[test_script_template.html]
+support-files =
+  file_script_template.html
+  file_script_template.js
new file mode 100644
--- /dev/null
+++ b/dom/security/test/csp/test_script_template.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Bug 1548385 - CSP: Test script template</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="testframe"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/**
+ * Description of the test:
+ * We load a document using a CSP of "default-src 'unsafe-inline'"
+ * and make sure that an external script within a template gets
+ * blocked correctly.
+ */
+
+const CSP_BLOCKED_SUBJECT = "csp-on-violate-policy";
+const CSP_ALLOWED_SUBJECT = "specialpowers-http-notify-request";
+
+SimpleTest.waitForExplicitFinish();
+
+function examiner() {
+  SpecialPowers.addObserver(this, CSP_BLOCKED_SUBJECT);
+  SpecialPowers.addObserver(this, CSP_ALLOWED_SUBJECT);
+}
+
+examiner.prototype  = {
+  observe: function(subject, topic, data) {
+    if (topic == CSP_BLOCKED_SUBJECT) {
+      let jsFileName = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
+      if (jsFileName.endsWith("file_script_template.js")) {
+        ok(true, "js file blocked by CSP");
+        this.removeAndFinish();
+      }
+    }
+
+    if (topic == CSP_ALLOWED_SUBJECT) {
+      if (data.endsWith("file_script_template.js")) {
+        ok(false, "js file allowed by CSP");
+        this.removeAndFinish();
+      }
+    }
+  },
+
+  removeAndFinish: function() {
+    SpecialPowers.removeObserver(this, CSP_BLOCKED_SUBJECT);
+    SpecialPowers.removeObserver(this, CSP_ALLOWED_SUBJECT);
+    SimpleTest.finish();
+  }
+}
+
+window.examiner = new examiner();
+document.getElementById("testframe").src = "file_script_template.html";
+
+</script>
+</body>
+</html>
--- a/dom/smil/test/mochitest.ini
+++ b/dom/smil/test/mochitest.ini
@@ -56,10 +56,11 @@ skip-if = toolkit == 'android' #TIMED_OU
 [test_smilSyncbaseTarget.xhtml]
 [test_smilTextZoom.xhtml]
 [test_smilTimeEvents.xhtml]
 [test_smilTiming.xhtml]
 [test_smilTimingZeroIntervals.xhtml]
 [test_smilUpdatedInterval.xhtml]
 [test_smilValues.xhtml]
 [test_smilWithTransition.html]
+skip-if = toolkit == 'android' && !is_fennec
 [test_smilWithXlink.xhtml]
 [test_smilXHR.xhtml]
--- a/dom/svg/SVGMPathElement.cpp
+++ b/dom/svg/SVGMPathElement.cpp
@@ -72,17 +72,17 @@ already_AddRefed<DOMSVGAnimatedString> S
 nsresult SVGMPathElement::BindToTree(Document* aDocument, nsIContent* aParent,
                                      nsIContent* aBindingParent) {
   MOZ_ASSERT(!mPathTracker.get(),
              "Shouldn't have href-target yet (or it should've been cleared)");
   nsresult rv =
       SVGMPathElementBase::BindToTree(aDocument, aParent, aBindingParent);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (aDocument) {
+  if (IsInComposedDoc()) {
     const nsAttrValue* hrefAttrValue =
         HasAttr(kNameSpaceID_None, nsGkAtoms::href)
             ? mAttrs.GetAttr(nsGkAtoms::href, kNameSpaceID_None)
             : mAttrs.GetAttr(nsGkAtoms::href, kNameSpaceID_XLink);
     if (hrefAttrValue) {
       UpdateHrefTarget(aParent, hrefAttrValue->GetStringValue());
     }
   }
@@ -98,17 +98,17 @@ void SVGMPathElement::UnbindFromTree(boo
 bool SVGMPathElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
                                      const nsAString& aValue,
                                      nsIPrincipal* aMaybeScriptedPrincipal,
                                      nsAttrValue& aResult) {
   bool returnVal = SVGMPathElementBase::ParseAttribute(
       aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult);
   if ((aNamespaceID == kNameSpaceID_XLink ||
        aNamespaceID == kNameSpaceID_None) &&
-      aAttribute == nsGkAtoms::href && IsInUncomposedDoc()) {
+      aAttribute == nsGkAtoms::href && IsInComposedDoc()) {
     // Note: If we fail the IsInDoc call, it's ok -- we'll update the target
     // on next BindToTree call.
 
     // Note: "href" takes priority over xlink:href. So if "xlink:href" is being
     // set here, we only let that update our target if "href" is *unset*.
     if (aNamespaceID != kNameSpaceID_XLink ||
         !mStringAttributes[HREF].IsExplicitlySet()) {
       UpdateHrefTarget(GetParent(), aValue);
--- a/dom/tests/mochitest/bugs/mochitest.ini
+++ b/dom/tests/mochitest/bugs/mochitest.ini
@@ -51,16 +51,17 @@ skip-if = toolkit == 'android'
 [test_bug304459.html]
 [test_bug308856.html]
 [test_bug327891.html]
 [test_bug333983.html]
 [test_bug335976.xhtml]
 [test_bug342448.html]
 [test_bug345521.html]
 [test_bug346659.html]
+skip-if = toolkit == 'android' && !is_fennec && debug
 [test_bug369306.html]
 skip-if = toolkit == 'android' #TIMED_OUT
 [test_bug370098.html]
 [test_bug377539.html]
 [test_bug384122.html]
 [test_bug389366.html]
 [test_bug393974.html]
 [test_bug394769.html]
--- a/dom/webauthn/U2FTokenManager.cpp
+++ b/dom/webauthn/U2FTokenManager.cpp
@@ -11,17 +11,17 @@
 #include "mozilla/dom/PWebAuthnTransactionParent.h"
 #include "mozilla/MozPromise.h"
 #include "mozilla/dom/WebAuthnUtil.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Unused.h"
 #include "nsTextFormatter.h"
 
-#ifdef ANDROID
+#ifdef MOZ_WIDGET_ANDROID
 #  include "mozilla/dom/AndroidWebAuthnTokenManager.h"
 #endif
 
 // Not named "security.webauth.u2f_softtoken_counter" because setting that
 // name causes the window.u2f object to disappear until preferences get
 // reloaded, as its pref is a substring!
 #define PREF_U2F_NSSTOKEN_COUNTER "security.webauth.softtoken_counter"
 #define PREF_WEBAUTHN_SOFTTOKEN_ENABLED \
@@ -263,17 +263,17 @@ RefPtr<U2FTokenTransport> U2FTokenManage
 
   if (!gBackgroundThread) {
     gBackgroundThread = NS_GetCurrentThread();
     MOZ_ASSERT(gBackgroundThread, "This should never be null!");
   }
 
   auto pm = U2FPrefManager::Get();
 
-#ifdef ANDROID
+#ifdef MOZ_WIDGET_ANDROID
   // On Android, prefer the platform support if enabled.
   if (pm->GetAndroidFido2Enabled()) {
     return AndroidWebAuthnTokenManager::GetInstance();
   }
 #endif
 
   // Prefer the HW token, even if the softtoken is enabled too.
   // We currently don't support soft and USB tokens enabled at the
--- a/dom/webauthn/moz.build
+++ b/dom/webauthn/moz.build
@@ -58,17 +58,17 @@ include('/ipc/chromium/chromium-config.m
 FINAL_LIBRARY = 'xul'
 
 LOCAL_INCLUDES += [
     '/dom/base',
     '/dom/crypto',
     '/security/manager/ssl',
 ]
 
-if CONFIG['OS_TARGET'] == 'Android':
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
     EXPORTS.mozilla.dom += [
         'AndroidWebAuthnTokenManager.h',
     ]
     UNIFIED_SOURCES += [
         'AndroidWebAuthnTokenManager.cpp',
     ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -2142,25 +2142,28 @@ WorkerPrivate::WorkerPrivate(WorkerPriva
 
   if (!target) {
     target = GetMainThreadSerialEventTarget();
     MOZ_DIAGNOSTIC_ASSERT(target);
   }
 
   // Throttle events to the main thread using a ThrottledEventQueue specific to
   // this tree of worker threads.
-  mMainThreadEventTargetForMessaging = ThrottledEventQueue::Create(target);
+  mMainThreadEventTargetForMessaging =
+      ThrottledEventQueue::Create(target, "Worker queue for messaging");
   if (StaticPrefs::dom_worker_use_medium_high_event_queue()) {
     mMainThreadEventTarget =
         ThrottledEventQueue::Create(GetMainThreadSerialEventTarget(),
+                                    "Worker queue",
                                     nsIRunnablePriority::PRIORITY_MEDIUMHIGH);
   } else {
     mMainThreadEventTarget = mMainThreadEventTargetForMessaging;
   }
-  mMainThreadDebuggeeEventTarget = ThrottledEventQueue::Create(target);
+  mMainThreadDebuggeeEventTarget =
+      ThrottledEventQueue::Create(target, "Worker debuggee queue");
   if (IsParentWindowPaused() || IsFrozen()) {
     MOZ_ALWAYS_SUCCEEDS(mMainThreadDebuggeeEventTarget->SetIsPaused(true));
   }
 }
 
 WorkerPrivate::~WorkerPrivate() {
   DropJSObjects(this);
 
--- a/dom/xhr/tests/mochitest.ini
+++ b/dom/xhr/tests/mochitest.ini
@@ -99,18 +99,19 @@ skip-if = toolkit == 'android'
 [test_xhr_overridemimetype_throws_on_invalid_state.html]
 [test_XHR_parameters.html]
 [test_xhr_progressevents.html]
 skip-if = toolkit == 'android'
 [test_xhr_send.html]
 [test_xhr_send_readystate.html]
 [test_XHR_system.html]
 [test_XHR_timeout.html]
-skip-if = (android_version == '18' && debug)
+skip-if = toolkit == 'android' && debug
 support-files = test_XHR_timeout.js
 [test_xhr_withCredentials.html]
 [test_XHRDocURI.html]
 [test_XHRResponseURL.html]
 [test_XHRSendData.html]
 [test_sync_xhr_document_write_with_iframe.html]
+skip-if = toolkit == "android" && debug && !is_fennec
 [test_nestedSyncXHR.html]
 [test_event_listener_leaks.html]
 skip-if = (os == "win" && processor == "aarch64") #bug 1535784
--- a/dom/xml/test/mochitest.ini
+++ b/dom/xml/test/mochitest.ini
@@ -9,9 +9,10 @@ support-files =
 [test_bug232004.xhtml]
 [test_bug293347.html]
 [test_bug343870.xhtml]
 [test_bug355213.xhtml]
 [test_bug392338.html]
 [test_bug399502.xhtml]
 [test_bug445330.html]
 [test_bug691215.html]
+skip-if = toolkit == "android" && debug && !is_fennec
 [test_viewport.xhtml]
--- a/editor/reftests/xul/emptytextbox-4.xul
+++ b/editor/reftests/xul/emptytextbox-4.xul
@@ -2,11 +2,11 @@
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml"
         title="Textbox tests">
 
   <script type="text/javascript" src="platform.js"/>
 
-  <textbox type="search"/>
-      
+  <textbox is="search-textbox"/>
+
 </window>
--- a/gfx/ipc/GPUParent.cpp
+++ b/gfx/ipc/GPUParent.cpp
@@ -59,16 +59,17 @@
 #endif
 #ifdef MOZ_WIDGET_GTK
 #  include <gtk/gtk.h>
 #  include "skia/include/ports/SkTypeface_cairo.h"
 #endif
 #ifdef MOZ_GECKO_PROFILER
 #  include "ChildProfilerController.h"
 #endif
+#include "nsAppRunner.h"
 
 namespace mozilla {
 namespace gfx {
 
 using namespace ipc;
 using namespace layers;
 
 static GPUParent* sGPUParent;
@@ -219,17 +220,26 @@ mozilla::ipc::IPCResult GPUParent::RecvI
     nsCOMPtr<nsIGfxInfo> gfxInfo = services::GetGfxInfo();
     Unused << gfxInfo;
 
     Factory::EnsureDWriteFactory();
   }
 #endif
 
 #if defined(MOZ_WIDGET_GTK)
-  char* display_name = PR_GetEnv("DISPLAY");
+  char* display_name = PR_GetEnv("MOZ_GDK_DISPLAY");
+  if (!display_name) {
+    bool waylandDisabled = true;
+#  ifdef MOZ_WAYLAND
+    waylandDisabled = IsWaylandDisabled();
+#  endif
+    if (waylandDisabled) {
+      display_name = PR_GetEnv("DISPLAY");
+    }
+  }
   if (display_name) {
     int argc = 3;
     char option_name[] = "--display";
     char* argv[] = {// argv0 is unused because g_set_prgname() was called in
                     // XRE_InitChildProcess().
                     nullptr, option_name, display_name, nullptr};
     char** argvp = argv;
     gtk_init(&argc, &argvp);
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -2658,32 +2658,53 @@ static FeatureState& WebRenderHardwareQu
               0x3ea0,
               0x3ea9,
               0x3ea2,
               0x3ea6,
               0x3ea7,
               0x3ea8,
               0x3ea5,
 
+              // broadwell gt2+
+              0x1612,
+              0x1616,
+              0x161a,
+              0x161b,
+              0x161d,
+              0x161e,
+              0x1622,
+              0x1626,
+              0x162a,
+              0x162b,
+              0x162d,
+              0x162e,
+              0x1632,
+              0x1636,
+              0x163a,
+              0x163b,
+              0x163d,
+              0x163e,
+
               // HD Graphics 4600
               0x0412,
               0x0416,
               0x041a,
               0x041b,
               0x041e,
               0x0a12,
               0x0a16,
               0x0a1a,
               0x0a1b,
               0x0a1e,
           };
           bool supported = false;
           for (uint16_t id : supportedDevices) {
             if (deviceID == id) {
               supported = true;
+              break;
             }
           }
           if (!supported) {
             featureWebRenderQualified.Disable(
                 FeatureStatus::BlockedDeviceTooOld, "Device too old",
                 NS_LITERAL_CSTRING("FEATURE_FAILURE_DEVICE_TOO_OLD"));
           }
 #  ifdef MOZ_WIDGET_GTK
--- a/intl/hyphenation/glue/nsHyphenationManager.cpp
+++ b/intl/hyphenation/glue/nsHyphenationManager.cpp
@@ -75,16 +75,19 @@ nsHyphenationManager::~nsHyphenationMana
 
 already_AddRefed<nsHyphenator> nsHyphenationManager::GetHyphenator(
     nsAtom* aLocale) {
   RefPtr<nsHyphenator> hyph;
   mHyphenators.Get(aLocale, getter_AddRefs(hyph));
   if (hyph) {
     return hyph.forget();
   }
+  nsAutoCString hyphCapPref("intl.hyphenate-capitalized.");
+  hyphCapPref.Append(nsAtomCString(aLocale));
+  bool hyphenateCapitalized = Preferences::GetBool(hyphCapPref.get());
   nsCOMPtr<nsIURI> uri = mPatternFiles.Get(aLocale);
   if (!uri) {
     RefPtr<nsAtom> alias = mHyphAliases.Get(aLocale);
     if (alias) {
       mHyphenators.Get(alias, getter_AddRefs(hyph));
       if (hyph) {
         return hyph.forget();
       }
@@ -106,17 +109,17 @@ already_AddRefed<nsHyphenator> nsHyphena
         localeStr.ReplaceLiteral(i, localeStr.Length() - i, "-*");
         RefPtr<nsAtom> fuzzyLocale = NS_Atomize(localeStr);
         return GetHyphenator(fuzzyLocale);
       } else {
         return nullptr;
       }
     }
   }
-  hyph = new nsHyphenator(uri);
+  hyph = new nsHyphenator(uri, hyphenateCapitalized);
   if (hyph->IsValid()) {
     mHyphenators.Put(aLocale, hyph);
     return hyph.forget();
   }
 #ifdef DEBUG
   nsCString msg("failed to load patterns from ");
   msg += uri->GetSpecOrDefault();
   NS_WARNING(msg.get());
--- a/intl/hyphenation/glue/nsHyphenator.cpp
+++ b/intl/hyphenation/glue/nsHyphenator.cpp
@@ -6,17 +6,18 @@
 #include "nsHyphenator.h"
 #include "nsIFile.h"
 #include "nsUTF8Utils.h"
 #include "nsUnicodeProperties.h"
 #include "nsIURI.h"
 
 #include "hyphen.h"
 
-nsHyphenator::nsHyphenator(nsIURI* aURI) : mDict(nullptr) {
+nsHyphenator::nsHyphenator(nsIURI* aURI, bool aHyphenateCapitalized)
+    : mDict(nullptr), mHyphenateCapitalized(aHyphenateCapitalized) {
   nsCString uriSpec;
   nsresult rv = aURI->GetSpec(uriSpec);
   if (NS_FAILED(rv)) {
     return;
   }
   mDict = hnj_hyphen_load(uriSpec.get());
 #ifdef DEBUG
   if (mDict) {
@@ -65,84 +66,100 @@ nsresult nsHyphenator::Hyphenate(const n
       }
       wordLimit = i + chLen;
       if (i + chLen < aString.Length()) {
         continue;
       }
     }
 
     if (inWord) {
-      // Convert the word to utf-8 for libhyphen, lowercasing it as we go
-      // so that it will match the (lowercased) patterns (bug 1105644).
-      nsAutoCString utf8;
-      const char16_t* const begin = aString.BeginReading();
-      const char16_t* cur = begin + wordStart;
-      const char16_t* end = begin + wordLimit;
-      while (cur < end) {
-        uint32_t ch = *cur++;
-
-        if (NS_IS_HIGH_SURROGATE(ch)) {
-          if (cur < end && NS_IS_LOW_SURROGATE(*cur)) {
-            ch = SURROGATE_TO_UCS4(ch, *cur++);
-          } else {
-            ch = 0xfffd;  // unpaired surrogate, treat as REPLACEMENT CHAR
-          }
-        } else if (NS_IS_LOW_SURROGATE(ch)) {
-          ch = 0xfffd;  // unpaired surrogate
-        }
-
-        // XXX What about language-specific casing? Consider Turkish I/i...
-        // In practice, it looks like the current patterns will not be
-        // affected by this, as they treat dotted and undotted i similarly.
-        ch = ToLowerCase(ch);
-
-        if (ch < 0x80) {  // U+0000 - U+007F
-          utf8.Append(ch);
-        } else if (ch < 0x0800) {  // U+0100 - U+07FF
-          utf8.Append(0xC0 | (ch >> 6));
-          utf8.Append(0x80 | (0x003F & ch));
-        } else if (ch < 0x10000) {  // U+0800 - U+D7FF,U+E000 - U+FFFF
-          utf8.Append(0xE0 | (ch >> 12));
-          utf8.Append(0x80 | (0x003F & (ch >> 6)));
-          utf8.Append(0x80 | (0x003F & ch));
-        } else {
-          utf8.Append(0xF0 | (ch >> 18));
-          utf8.Append(0x80 | (0x003F & (ch >> 12)));
-          utf8.Append(0x80 | (0x003F & (ch >> 6)));
-          utf8.Append(0x80 | (0x003F & ch));
-        }
-      }
-
-      AutoTArray<char, 200> utf8hyphens;
-      utf8hyphens.SetLength(utf8.Length() + 5);
-      char** rep = nullptr;
-      int* pos = nullptr;
-      int* cut = nullptr;
-      int err = hnj_hyphen_hyphenate2((HyphenDict*)mDict, utf8.BeginReading(),
-                                      utf8.Length(), utf8hyphens.Elements(),
-                                      nullptr, &rep, &pos, &cut);
-      if (!err) {
-        // Surprisingly, hnj_hyphen_hyphenate2 converts the 'hyphens' buffer
-        // from utf8 code unit indexing (which would match the utf8 input
-        // string directly) to Unicode character indexing.
-        // We then need to convert this to utf16 code unit offsets for Gecko.
-        const char* hyphPtr = utf8hyphens.Elements();
-        const char16_t* cur = begin + wordStart;
-        const char16_t* end = begin + wordLimit;
-        while (cur < end) {
-          if (*hyphPtr & 0x01) {
-            aHyphens[cur - begin] = true;
-          }
-          cur++;
-          if (cur < end && NS_IS_LOW_SURROGATE(*cur) &&
-              NS_IS_HIGH_SURROGATE(*(cur - 1))) {
-            cur++;
-          }
-          hyphPtr++;
-        }
-      }
+      HyphenateWord(aString, wordStart, wordLimit, aHyphens);
+      inWord = false;
     }
-
-    inWord = false;
   }
 
   return NS_OK;
 }
+
+void nsHyphenator::HyphenateWord(const nsAString& aString, uint32_t aStart,
+                                 uint32_t aLimit, nsTArray<bool>& aHyphens) {
+  // Convert word from aStart and aLimit in aString to utf-8 for libhyphen,
+  // lowercasing it as we go so that it will match the (lowercased) patterns
+  // (bug 1105644).
+  nsAutoCString utf8;
+  const char16_t* const begin = aString.BeginReading();
+  const char16_t* cur = begin + aStart;
+  const char16_t* end = begin + aLimit;
+  bool firstLetter = true;
+  while (cur < end) {
+    uint32_t ch = *cur++;
+
+    if (NS_IS_HIGH_SURROGATE(ch)) {
+      if (cur < end && NS_IS_LOW_SURROGATE(*cur)) {
+        ch = SURROGATE_TO_UCS4(ch, *cur++);
+      } else {
+        ch = 0xfffd;  // unpaired surrogate, treat as REPLACEMENT CHAR
+      }
+    } else if (NS_IS_LOW_SURROGATE(ch)) {
+      ch = 0xfffd;  // unpaired surrogate
+    }
+
+    // XXX What about language-specific casing? Consider Turkish I/i...
+    // In practice, it looks like the current patterns will not be
+    // affected by this, as they treat dotted and undotted i similarly.
+    uint32_t origCh = ch;
+    ch = ToLowerCase(ch);
+
+    // Avoid hyphenating capitalized words (bug 1550532) unless explicitly
+    // allowed by prefs for the language in use.
+    if (firstLetter) {
+      if (!mHyphenateCapitalized && ch != origCh) {
+        return;
+      }
+      firstLetter = false;
+    }
+
+    if (ch < 0x80) {  // U+0000 - U+007F
+      utf8.Append(ch);
+    } else if (ch < 0x0800) {  // U+0100 - U+07FF
+      utf8.Append(0xC0 | (ch >> 6));
+      utf8.Append(0x80 | (0x003F & ch));
+    } else if (ch < 0x10000) {  // U+0800 - U+D7FF,U+E000 - U+FFFF
+      utf8.Append(0xE0 | (ch >> 12));
+      utf8.Append(0x80 | (0x003F & (ch >> 6)));
+      utf8.Append(0x80 | (0x003F & ch));
+    } else {
+      utf8.Append(0xF0 | (ch >> 18));
+      utf8.Append(0x80 | (0x003F & (ch >> 12)));
+      utf8.Append(0x80 | (0x003F & (ch >> 6)));
+      utf8.Append(0x80 | (0x003F & ch));
+    }
+  }
+
+  AutoTArray<char, 200> utf8hyphens;
+  utf8hyphens.SetLength(utf8.Length() + 5);
+  char** rep = nullptr;
+  int* pos = nullptr;
+  int* cut = nullptr;
+  int err = hnj_hyphen_hyphenate2((HyphenDict*)mDict, utf8.BeginReading(),
+                                  utf8.Length(), utf8hyphens.Elements(),
+                                  nullptr, &rep, &pos, &cut);
+  if (!err) {
+    // Surprisingly, hnj_hyphen_hyphenate2 converts the 'hyphens' buffer
+    // from utf8 code unit indexing (which would match the utf8 input
+    // string directly) to Unicode character indexing.
+    // We then need to convert this to utf16 code unit offsets for Gecko.
+    const char* hyphPtr = utf8hyphens.Elements();
+    const char16_t* cur = begin + aStart;
+    const char16_t* end = begin + aLimit;
+    while (cur < end) {
+      if (*hyphPtr & 0x01) {
+        aHyphens[cur - begin] = true;
+      }
+      cur++;
+      if (cur < end && NS_IS_LOW_SURROGATE(*cur) &&
+          NS_IS_HIGH_SURROGATE(*(cur - 1))) {
+        cur++;
+      }
+      hyphPtr++;
+    }
+  }
+}
--- a/intl/hyphenation/glue/nsHyphenator.h
+++ b/intl/hyphenation/glue/nsHyphenator.h
@@ -9,24 +9,27 @@
 #include "nsCOMPtr.h"
 #include "nsString.h"
 #include "nsTArray.h"
 
 class nsIURI;
 
 class nsHyphenator {
  public:
-  explicit nsHyphenator(nsIURI* aURI);
+  nsHyphenator(nsIURI* aURI, bool aHyphenateCapitalized);
 
   NS_INLINE_DECL_REFCOUNTING(nsHyphenator)
 
   bool IsValid();
 
   nsresult Hyphenate(const nsAString& aText, nsTArray<bool>& aHyphens);
 
  private:
   ~nsHyphenator();
 
- protected:
+  void HyphenateWord(const nsAString& aString, uint32_t aStart,
+                     uint32_t aLimit, nsTArray<bool>& aHyphens);
+
   void* mDict;
+  bool mHyphenateCapitalized;
 };
 
 #endif  // nsHyphenator_h__
--- a/js/public/GCVector.h
+++ b/js/public/GCVector.h
@@ -299,11 +299,22 @@ template <typename T>
 class RootedVector : public Rooted<StackGCVector<T>> {
   using Vec = StackGCVector<T>;
   using Base = Rooted<Vec>;
 
  public:
   explicit RootedVector(JSContext* cx) : Base(cx, Vec(cx)) {}
 };
 
+// For use in rust code, an analog to RootedVector that doesn't require
+// instances to be destroyed in LIFO order.
+template <typename T>
+class PersistentRootedVector : public PersistentRooted<StackGCVector<T>> {
+  using Vec = StackGCVector<T>;
+  using Base = PersistentRooted<Vec>;
+
+ public:
+  explicit PersistentRootedVector(JSContext* cx) : Base(cx, Vec(cx)) {}
+};
+
 }  // namespace JS
 
 #endif  // js_GCVector_h
--- a/js/public/PropertySpec.h
+++ b/js/public/PropertySpec.h
@@ -14,17 +14,17 @@
 #include <stdint.h>     // uint8_t, uint16_t, int32_t, uint32_t, uintptr_t
 #include <type_traits>  // std::enable_if
 
 #include "jstypes.h"  // JS_PUBLIC_API
 
 #include "js/CallArgs.h"            // JSNative
 #include "js/PropertyDescriptor.h"  // JSPROP_*
 #include "js/RootingAPI.h"          // JS::MutableHandle
-#include "js/Symbol.h"              // JS::SymbolCode
+#include "js/Symbol.h"              // JS::SymbolCode, PropertySpecNameIsSymbol
 #include "js/Value.h"               // JS::Value
 
 struct JSContext;
 struct JSJitInfo;
 
 /**
  * Wrapper to relace JSNative for JSPropertySpecs and JSFunctionSpecs. This will
  * allow us to pass one JSJitInfo per function with the property/function spec,
@@ -143,66 +143,133 @@ struct JSPropertySpec {
       return AccessorsOrValue(getter, setter);
     }
 
     static constexpr AccessorsOrValue fromValue(ValueWrapper value) {
       return AccessorsOrValue(value);
     }
   };
 
-  const char* name;
+  union Name {
+   private:
+    const char* string_;
+    uintptr_t symbol_;
+
+   public:
+    Name() = delete;
+
+    explicit constexpr Name(const char* str) : string_(str) {}
+    explicit constexpr Name(JS::SymbolCode symbol)
+        : symbol_(uint32_t(symbol) + 1) {}
+
+    explicit operator bool() const { return !!symbol_; }
+
+    bool isSymbol() const { return JS::PropertySpecNameIsSymbol(symbol_); }
+    JS::SymbolCode symbol() const {
+      MOZ_ASSERT(isSymbol());
+      return JS::SymbolCode(symbol_ - 1);
+    }
+
+    bool isString() const { return !isSymbol(); }
+    const char* string() const {
+      MOZ_ASSERT(isString());
+      return string_;
+    }
+  };
+
+  Name name;
   uint8_t flags;
   AccessorsOrValue u;
 
  private:
   JSPropertySpec() = delete;
 
   constexpr JSPropertySpec(const char* name, uint8_t flags, AccessorsOrValue u)
       : name(name), flags(flags), u(u) {}
+  constexpr JSPropertySpec(JS::SymbolCode name, uint8_t flags,
+                           AccessorsOrValue u)
+      : name(name), flags(flags), u(u) {}
 
  public:
   JSPropertySpec(const JSPropertySpec& other) = default;
 
   static constexpr JSPropertySpec nativeAccessors(
       const char* name, uint8_t flags, JSNative getter,
       const JSJitInfo* getterInfo, JSNative setter = nullptr,
       const JSJitInfo* setterInfo = nullptr) {
     return JSPropertySpec(
         name, flags,
         AccessorsOrValue::fromAccessors(
             JSPropertySpec::Accessor::nativeAccessor(getter, getterInfo),
             JSPropertySpec::Accessor::nativeAccessor(setter, setterInfo)));
   }
 
+  static constexpr JSPropertySpec nativeAccessors(
+      JS::SymbolCode name, uint8_t flags, JSNative getter,
+      const JSJitInfo* getterInfo, JSNative setter = nullptr,
+      const JSJitInfo* setterInfo = nullptr) {
+    return JSPropertySpec(
+        name, flags,
+        AccessorsOrValue::fromAccessors(
+            JSPropertySpec::Accessor::nativeAccessor(getter, getterInfo),
+            JSPropertySpec::Accessor::nativeAccessor(setter, setterInfo)));
+  }
+
   static constexpr JSPropertySpec selfHostedAccessors(
       const char* name, uint8_t flags, const char* getterName,
       const char* setterName = nullptr) {
     return JSPropertySpec(
         name, flags | JSPROP_GETTER | (setterName ? JSPROP_