Bug 1412091 - Export Screenshots 23.0.0 to Firefox (all except translations and vendor library update) draft
authorIan Bicking <ianb@colorstudy.com>
Fri, 27 Oct 2017 13:32:59 -0500
changeset 697273 e27f7dbebf6dc243cc6e4edfe92e5f14fa03de3a
parent 687768 3398894c0746c76d1768d2fd8bfdccac38b8c0b5
child 740076 ae49e16222e623365f24300481abc92cefcc450e
push id88952
push userbmo:ianb@mozilla.com
push dateMon, 13 Nov 2017 20:20:15 +0000
bugs1412091
milestone58.0a1
Bug 1412091 - Export Screenshots 23.0.0 to Firefox (all except translations and vendor library update) MozReview-Commit-ID: BtJ3vwqWTHA
browser/extensions/screenshots/bootstrap.js
browser/extensions/screenshots/install.rdf
browser/extensions/screenshots/moz.build
browser/extensions/screenshots/test/browser/.eslintrc.yml
browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
browser/extensions/screenshots/webextension/background/auth.js
browser/extensions/screenshots/webextension/background/selectorLoader.js
browser/extensions/screenshots/webextension/background/takeshot.js
browser/extensions/screenshots/webextension/blobConverters.js
browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
browser/extensions/screenshots/webextension/build/onboardingCss.js
browser/extensions/screenshots/webextension/build/shot.js
browser/extensions/screenshots/webextension/domainFromUrl.js
browser/extensions/screenshots/webextension/manifest.json
browser/extensions/screenshots/webextension/selector/ui.js
browser/extensions/screenshots/webextension/selector/uicontrol.js
browser/extensions/screenshots/webextension/sitehelper.js
--- a/browser/extensions/screenshots/bootstrap.js
+++ b/browser/extensions/screenshots/bootstrap.js
@@ -1,11 +1,9 @@
 /* globals ADDON_DISABLE */
-// TODO: re-enable
-/* eslint-disable */
 const OLD_ADDON_PREF_NAME = "extensions.jid1-NeEaf3sAHdKHPA@jetpack.deviceIdInfo";
 const OLD_ADDON_ID = "jid1-NeEaf3sAHdKHPA@jetpack";
 const ADDON_ID = "screenshots@mozilla.org";
 const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
 const PREF_BRANCH = "extensions.screenshots.";
 const USER_DISABLE_PREF = "extensions.screenshots.disabled";
 
 const { interfaces: Ci, utils: Cu } = Components;
--- a/browser/extensions/screenshots/install.rdf
+++ b/browser/extensions/screenshots/install.rdf
@@ -7,14 +7,14 @@
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
         <em:minVersion>57.0a1</em:minVersion>
         <em:maxVersion>*</em:maxVersion>
       </Description>
     </em:targetApplication>
     <em:type>2</em:type>
-    <em:version>19.2.0</em:version>
+    <em:version>23.0.0</em:version>
     <em:bootstrap>true</em:bootstrap>
     <em:homepageURL>https://screenshots.firefox.com/</em:homepageURL>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
   </Description>
 </RDF>
--- a/browser/extensions/screenshots/moz.build
+++ b/browser/extensions/screenshots/moz.build
@@ -52,16 +52,20 @@ FINAL_TARGET_FILES.features['screenshots
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["bg"] += [
   'webextension/_locales/bg/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["bn_BD"] += [
   'webextension/_locales/bn_BD/messages.json'
 ]
 
+FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["bs"] += [
+  'webextension/_locales/bs/messages.json'
+]
+
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["ca"] += [
   'webextension/_locales/ca/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["cak"] += [
   'webextension/_locales/cak/messages.json'
 ]
 
@@ -124,16 +128,20 @@ FINAL_TARGET_FILES.features['screenshots
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["eu"] += [
   'webextension/_locales/eu/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["fa"] += [
   'webextension/_locales/fa/messages.json'
 ]
 
+FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["ff"] += [
+  'webextension/_locales/ff/messages.json'
+]
+
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["fi"] += [
   'webextension/_locales/fi/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["fr"] += [
   'webextension/_locales/fr/messages.json'
 ]
 
@@ -196,16 +204,20 @@ FINAL_TARGET_FILES.features['screenshots
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["kk"] += [
   'webextension/_locales/kk/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["km"] += [
   'webextension/_locales/km/messages.json'
 ]
 
+FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["kn"] += [
+  'webextension/_locales/kn/messages.json'
+]
+
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["ko"] += [
   'webextension/_locales/ko/messages.json'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["_locales"]["lij"] += [
   'webextension/_locales/lij/messages.json'
 ]
 
--- a/browser/extensions/screenshots/test/browser/.eslintrc.yml
+++ b/browser/extensions/screenshots/test/browser/.eslintrc.yml
@@ -1,10 +1,5 @@
 env:
   node: true
 
-# TODO: re-enable
-#extends:
-#  - plugin:mozilla/browser-test
-
-rules:
-  no-unused-vars: off
-  no-undef: off
+extends:
+  - plugin:mozilla/browser-test
--- a/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
+++ b/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
@@ -1,9 +1,8 @@
-/* eslint disable */
 "use strict";
 
 const BUTTON_ID = "pageAction-panel-screenshots";
 
 function checkElements(expectPresent, l) {
   for (let id of l) {
     is(!!document.getElementById(id), expectPresent, "element " + id + (expectPresent ? " is" : " is not") + " present");
   }
--- a/browser/extensions/screenshots/webextension/background/auth.js
+++ b/browser/extensions/screenshots/webextension/background/auth.js
@@ -195,20 +195,27 @@ this.auth = (function() {
       return browser.storage.local.set({registrationInfo}).then(() => {
         return true;
       });
     });
   };
 
   communication.register("getAuthInfo", (sender, ownershipCheck) => {
     return registrationInfoFetched.then(() => {
+      return exports.authHeaders();
+    }).then((authHeaders) => {
       let info = registrationInfo;
       if (info.registered) {
         return login({ownershipCheck}).then((result) => {
-          return {isOwner: result && result.isOwner, deviceId: registrationInfo.deviceId};
+          return {
+            isOwner: result && result.isOwner,
+            deviceId: registrationInfo.deviceId,
+            authHeaders
+          };
         });
       }
+      info = Object.assign({authHeaders}, info);
       return info;
     });
   });
 
   return exports;
 })();
--- a/browser/extensions/screenshots/webextension/background/selectorLoader.js
+++ b/browser/extensions/screenshots/webextension/background/selectorLoader.js
@@ -82,17 +82,17 @@ this.selectorLoader = (function() {
   };
 
   function executeModules(tabId, scripts) {
     let lastPromise = Promise.resolve(null);
     scripts.forEach((file) => {
       lastPromise = lastPromise.then(() => {
         return browser.tabs.executeScript(tabId, {
           file,
-          runAt: "document_end"
+          runAt: "document_start"
         }).catch((error) => {
           log.error("error in script:", file, error);
           error.scriptName = file;
           throw error;
         });
       });
     });
     return lastPromise.then(() => {
--- a/browser/extensions/screenshots/webextension/background/takeshot.js
+++ b/browser/extensions/screenshots/webextension/background/takeshot.js
@@ -147,17 +147,17 @@ this.takeshot = (function() {
       body.push(`Content-Disposition: form-data; name="${fileField}"; filename="${fileFilename}"`);
       body.push(`Content-Type: ${blob.type}`);
       body.push("");
       body.push("");
       body = body.join("\r\n");
       let enc = new TextEncoder("utf-8");
       body = enc.encode(body);
       body = concatBuffers(body.buffer, blobAsBuffer);
-      let tail = "\r\n" + "--" + boundary + "--";
+      let tail = `\r\n--${boundary}--`;
       tail = enc.encode(tail);
       body = concatBuffers(body, tail.buffer);
       return {
         "content-type": `multipart/form-data; boundary=${boundary}`,
         body
       };
     });
   }
@@ -166,22 +166,22 @@ this.takeshot = (function() {
     let headers;
     return auth.authHeaders().then((_headers) => {
       headers = _headers;
       if (blob) {
         return createMultipart(
           {shot: JSON.stringify(shot.asJson())},
           "blob", "screenshot.png", blob
         );
-      } else {
-        return {
-          "content-type": "application/json",
-          body: JSON.stringify(shot.asJson())
-        };
       }
+      return {
+        "content-type": "application/json",
+        body: JSON.stringify(shot.asJson())
+      };
+
     }).then((submission) => {
       headers["content-type"] = submission["content-type"];
       sendEvent("upload", "started", {eventValue: Math.floor(submission.body.length / 1000)});
       return fetch(shot.jsonUrl, {
         method: "PUT",
         mode: "cors",
         headers,
         body: submission.body
--- a/browser/extensions/screenshots/webextension/blobConverters.js
+++ b/browser/extensions/screenshots/webextension/blobConverters.js
@@ -1,9 +1,9 @@
-this.blobConverters = (function () {
+this.blobConverters = (function() {
   let exports = {};
 
   exports.dataUrlToBlob = function(url) {
     const binary = atob(url.split(',', 2)[1]);
     let contentType = exports.getTypeFromDataUrl(url);
     if (contentType != "image/png" && contentType != "image/jpeg") {
       contentType = "image/png";
     }
--- a/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
+++ b/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
@@ -252,16 +252,19 @@ window.inlineSelectionCss = `
   .hover-highlight::before {
     border: 2px dashed rgba(255, 255, 255, 0.4);
     bottom: 0;
     content: "";
     left: 0;
     position: absolute;
     right: 0;
     top: 0; }
+  body.hcm .hover-highlight {
+    background-color: white;
+    opacity: 0.2; }
 
 .mover-target.direction-topLeft {
   cursor: nwse-resize;
   height: 60px;
   left: -30px;
   top: -30px;
   width: 60px; }
 
@@ -353,38 +356,47 @@ window.inlineSelectionCss = `
 .direction-bottom .mover,
 .direction-bottomLeft .mover {
   bottom: -1px; }
 
 .bghighlight {
   background-color: rgba(0, 0, 0, 0.7);
   position: absolute;
   z-index: 9999999999; }
+  body.hcm .bghighlight {
+    background-color: black;
+    opacity: 0.7; }
 
 .preview-overlay {
   align-items: center;
   background-color: rgba(0, 0, 0, 0.7);
   display: flex;
   height: 100%;
   justify-content: center;
   left: 0;
   margin: 0;
   padding: 0;
   position: fixed;
   top: 0;
   width: 100%;
   z-index: 9999999999; }
+  body.hcm .preview-overlay {
+    background-color: black;
+    opacity: 0.7; }
 
 .highlight {
   border-radius: 2px;
   border: 2px dashed rgba(255, 255, 255, 0.8);
   box-sizing: border-box;
   cursor: move;
   position: absolute;
   z-index: 9999999999; }
+  body.hcm .highlight {
+    border: 2px dashed white;
+    opacity: 1.0; }
 
 .highlight-buttons {
   display: flex;
   align-items: center;
   justify-content: center;
   bottom: -55px;
   position: absolute;
   z-index: 6; }
--- a/browser/extensions/screenshots/webextension/build/onboardingCss.js
+++ b/browser/extensions/screenshots/webextension/build/onboardingCss.js
@@ -252,16 +252,22 @@ body {
     background-size: 24px 24px; }
 
 #next {
   background-image: url("MOZ_EXTENSION/icons/back.svg");
   transform: rotate(180deg); }
   .active-slide-1 #next {
     background-image: url("MOZ_EXTENSION/icons/back-highlight.svg"); }
 
+[dir='rtl'] #next {
+  transform: rotate(0deg); }
+
+[dir='rtl'] #prev {
+  transform: rotate(180deg); }
+
 #skip {
   background: none;
   border: 0;
   color: #fff;
   font-size: 16px;
   left: 50%;
   margin-left: -330px;
   margin-top: 257px;
--- a/browser/extensions/screenshots/webextension/build/shot.js
+++ b/browser/extensions/screenshots/webextension/build/shot.js
@@ -24,17 +24,17 @@ function isUrl(url) {
     return true;
   }
   if ((/^chrome:.{0,8000}/i).test(url)) {
     return true;
   }
   if ((/^view-source:/i).test(url)) {
     return isUrl(url.substr("view-source:".length));
   }
-  return (/^https?:\/\/[a-z0-9.-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
+  return (/^https?:\/\/[a-z0-9.-_]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
 }
 
 function isValidClipImageUrl(url) {
     return isUrl(url) && !(url.indexOf(')') > -1);
 }
 
 function assertUrl(url) {
   if (!url) {
@@ -43,17 +43,17 @@ function assertUrl(url) {
   if (!isUrl(url)) {
     let exc = new Error("Not a URL");
     exc.scheme = url.split(":")[0];
     throw exc;
   }
 }
 
 function isSecureWebUri(url) {
-  return (/^https?:\/\/[a-z0-9.-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
+  return (/^https?:\/\/[a-z0-9.-_]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
 }
 
 function assertOrigin(url) {
   assertUrl(url);
   if (url.search(/^https?:/i) != -1) {
     let match = (/^https?:\/\/[^/:]{1,4000}\/?$/i).exec(url);
     if (!match) {
       throw new Error("Bad origin, might include path");
@@ -124,17 +124,17 @@ function resolveUrl(base, url) {
     return url;
   }
   if (url.indexOf("//") === 0) {
     // Protocol-relative URL
     return (/^https?:/i).exec(base)[0] + url;
   }
   if (url.indexOf("/") === 0) {
     // Domain-relative URL
-    return (/^https?:\/\/[a-z0-9.-]{1,4000}/i).exec(base)[0] + url;
+    return (/^https?:\/\/[a-z0-9.-_]{1,4000}/i).exec(base)[0] + url;
   }
   // Otherwise, a full relative URL
   while (url.indexOf("./") === 0) {
     url = url.substr(2);
   }
   if (!base) {
     // It's not an absolute URL, and we don't have a base URL, so we have
     // to throw away the URL
@@ -204,17 +204,17 @@ function makeRandomId() {
   }
   return id;
 }
 
 class AbstractShot {
 
   constructor(backend, id, attrs) {
     attrs = attrs || {};
-    assert((/^[a-zA-Z0-9]{1,4000}\/[a-z0-9.-]{1,4000}$/).test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id));
+    assert((/^[a-zA-Z0-9]{1,4000}\/[a-z0-9.-_]{1,4000}$/).test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id));
     this._backend = backend;
     this._id = id;
     this.origin = attrs.origin || null;
     this.fullUrl = attrs.fullUrl || null;
     if ((!attrs.fullUrl) && attrs.url) {
       console.warn("Received deprecated attribute .url");
       this.fullUrl = attrs.url;
     }
@@ -360,17 +360,18 @@ class AbstractShot {
       assertOrigin(val);
     }
     this._origin = val || undefined;
   }
 
   get filename() {
     let filenameTitle = this.title;
     let date = new Date(this.createdDate);
-    filenameTitle = filenameTitle.replace(/[:\\<>/!@&?"*.|\n\r\t]/g, " ");
+    // eslint-disable-next-line no-control-regex
+    filenameTitle = filenameTitle.replace(/[:\\<>/!@&?"*.|\x00-\x1F]/g, " ");
     filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " ");
     let clipFilename = `Screenshot-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${filenameTitle}`;
     const clipFilenameBytesSize = clipFilename.length * 2; // JS STrings are UTF-16
     if (clipFilenameBytesSize > 251) { // 255 bytes (Usual filesystems max) - 4 for the ".png" file extension string
       const excedingchars = (clipFilenameBytesSize - 246) / 2; // 251 - 5 for ellipsis "[...]"
       clipFilename = clipFilename.substring(0, clipFilename.length - excedingchars);
       clipFilename = clipFilename + '[...]';
     }
--- a/browser/extensions/screenshots/webextension/domainFromUrl.js
+++ b/browser/extensions/screenshots/webextension/domainFromUrl.js
@@ -9,20 +9,20 @@ this.domainFromUrl = (function() {
   return function urlDomainForId(location) { // eslint-disable-line no-unused-vars
     let domain = location.hostname;
     if (!domain) {
       domain = location.origin.split(":")[0];
       if (!domain) {
         domain = "unknown";
       }
     }
-    if (domain.search(/^[a-z0-9.-]{1,1000}$/i) === -1) {
+    if (domain.search(/^[a-z0-9.-_]{1,1000}$/i) === -1) {
       // Probably a unicode domain; we could use punycode but it wouldn't decode
       // well in the URL anyway.  Instead we'll punt.
-      domain = domain.replace(/[^a-z0-9.-]/ig, "");
+      domain = domain.replace(/[^a-z0-9.-_]/ig, "");
       if (!domain) {
         domain = "site";
       }
     }
     return domain;
   };
 
 })();
--- a/browser/extensions/screenshots/webextension/manifest.json
+++ b/browser/extensions/screenshots/webextension/manifest.json
@@ -1,12 +1,12 @@
 {
   "manifest_version": 2,
   "name": "Firefox Screenshots",
-  "version": "19.2.0",
+  "version": "23.0.0",
   "description": "__MSG_addonDescription__",
   "author": "__MSG_addonAuthorsList__",
   "homepage_url": "https://github.com/mozilla-services/screenshots",
   "applications": {
     "gecko": {
       "id": "screenshots@mozilla.org",
       "strict_min_version": "57.0a1"
     }
--- a/browser/extensions/screenshots/webextension/selector/ui.js
+++ b/browser/extensions/screenshots/webextension/selector/ui.js
@@ -71,16 +71,31 @@ this.ui = (function() { // eslint-disabl
     let els = doc.querySelectorAll("[data-l10n-id]");
     for (let el of els) {
       let id = el.getAttribute("data-l10n-id");
       let text = browser.i18n.getMessage(id);
       el.textContent = text;
     }
   }
 
+  function highContrastCheck(win) {
+    let result, doc, el;
+    doc = win.document;
+    el = doc.createElement("div");
+    el.style.backgroundImage = "url('#')";
+    el.style.display = "none";
+    doc.body.appendChild(el);
+    let computed = win.getComputedStyle(el);
+    // When Windows is in High Contrast mode, Firefox replaces background
+    // image URLs with the string "none".
+    result = computed && computed.backgroundImage === "none";
+    doc.body.removeChild(el);
+    return result;
+  }
+
   function initializeIframe() {
     let el = document.createElement("iframe");
     el.src = browser.extension.getURL("blank.html");
     el.style.zIndex = "99999999999";
     el.style.border = "none";
     el.style.top = "0";
     el.style.left = "0";
     el.style.margin = "0";
@@ -135,16 +150,19 @@ this.ui = (function() { // eslint-disabl
     hide() {
       this.element.style.display = "none";
       this.stopSizeWatch();
     },
 
     unhide() {
       this.updateElementSize();
       this.element.style.display = "";
+      if (highContrastCheck(this.element.contentWindow)) {
+        this.element.contentDocument.body.classList.add("hcm");
+      }
       this.initSizeWatch();
       this.element.focus();
     },
 
     updateElementSize(force) {
       // Note: if someone sizes down the page, then the iframe will keep the
       // document from naturally shrinking.  We use force to temporarily hide
       // the element so that we can tell if the document shrinks
@@ -285,16 +303,19 @@ this.ui = (function() { // eslint-disabl
         this.element.style.display = "none";
       }
     },
 
     unhide() {
       window.addEventListener("scroll", watchFunction(assertIsTrusted(this.onScroll)));
       window.addEventListener("resize", this.onResize, true);
       this.element.style.display = "";
+      if (highContrastCheck(this.element.contentWindow)) {
+        this.element.contentDocument.body.classList.add("hcm");
+      }
       this.element.focus();
     },
 
     onScroll() {
       exports.HoverBox.hide();
     },
 
     getElementFromPoint(x, y) {
--- a/browser/extensions/screenshots/webextension/selector/uicontrol.js
+++ b/browser/extensions/screenshots/webextension/selector/uicontrol.js
@@ -156,45 +156,46 @@ this.uicontrol = (function() {
         window.scrollX, window.scrollY,
         window.scrollX + window.innerWidth, window.scrollY + window.innerHeight);
       captureType = 'visible';
       setState("previewing");
     },
     onClickFullPage: () => {
       sendEvent("capture-full-page", "selection-button");
       captureType = "fullPage";
-      let width = Math.max(
-        document.body.clientWidth,
-        document.documentElement.clientWidth,
-        document.body.scrollWidth,
-        document.documentElement.scrollWidth);
+      let width = getDocumentWidth();
       if (width > MAX_PAGE_WIDTH) {
         captureType = "fullPageTruncated";
       }
       width = Math.min(width, MAX_PAGE_WIDTH);
-      let height = Math.max(
-        document.body.clientHeight,
-        document.documentElement.clientHeight,
-        document.body.scrollHeight,
-        document.documentElement.scrollHeight);
+      let height = getDocumentHeight();
       if (height > MAX_PAGE_HEIGHT) {
         captureType = "fullPageTruncated";
       }
       height = Math.min(height, MAX_PAGE_HEIGHT);
       selectedPos = new Selection(
         0, 0,
         width, height);
       setState("previewing");
     },
     onSavePreview: () => {
       sendEvent(`save-${captureType.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`, "save-preview-button");
       shooter.takeShot(captureType, selectedPos, dataUrl);
     },
     onDownloadPreview: () => {
       sendEvent(`download-${captureType.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`, "download-preview-button");
+
+      // Downloaded shots don't have dimension limits
+      if (captureType === "fullPageTruncated") {
+        captureType = "fullPage";
+        selectedPos = new Selection(
+          0, 0,
+          getDocumentWidth(), getDocumentHeight());
+      }
+
       shooter.downloadShot(selectedPos);
     }
   };
 
   /** Holds all the objects that handle events for each state: */
   let stateHandlers = {};
 
   function getState() {
@@ -456,16 +457,21 @@ this.uicontrol = (function() {
       let attemptExtend = false;
       let node = el;
       while (node) {
         rect = Selection.getBoundingClientRect(node);
         if (!rect) {
           rect = lastRect;
           break;
         }
+        if (rect.width < MIN_DETECT_WIDTH || rect.height < MIN_DETECT_HEIGHT) {
+          // Avoid infinite loop for elements with zero or nearly zero height,
+          // like non-clearfixed float parents with or without borders.
+          break;
+        }
         if (rect.width > MAX_DETECT_WIDTH || rect.height > MAX_DETECT_HEIGHT) {
           // Then the last rectangle is better
           rect = lastRect;
           attemptExtend = true;
           break;
         }
         if (rect.width >= MIN_DETECT_WIDTH && rect.height >= MIN_DETECT_HEIGHT) {
           if (!doNotAutoselectTags[node.tagName]) {
@@ -811,38 +817,42 @@ this.uicontrol = (function() {
 
   stateHandlers.cancel = {
     start() {
       ui.iframe.hide();
       ui.Box.remove();
     }
   };
 
-  let documentWidth = Math.max(
-    document.body && document.body.clientWidth,
-    document.documentElement.clientWidth,
-    document.body && document.body.scrollWidth,
-    document.documentElement.scrollWidth);
-  let documentHeight = Math.max(
-    document.body && document.body.clientHeight,
-    document.documentElement.clientHeight,
-    document.body && document.body.scrollHeight,
-    document.documentElement.scrollHeight);
+  function getDocumentWidth() {
+    return Math.max(
+      document.body && document.body.clientWidth,
+      document.documentElement.clientWidth,
+      document.body && document.body.scrollWidth,
+      document.documentElement.scrollWidth);
+  }
+  function getDocumentHeight() {
+    return Math.max(
+      document.body && document.body.clientHeight,
+      document.documentElement.clientHeight,
+      document.body && document.body.scrollHeight,
+      document.documentElement.scrollHeight);
+  }
 
   function scrollIfByEdge(pageX, pageY) {
     let top = window.scrollY;
     let bottom = top + window.innerHeight;
     let left = window.scrollX;
     let right = left + window.innerWidth;
-    if (pageY + SCROLL_BY_EDGE >= bottom && bottom < documentHeight) {
+    if (pageY + SCROLL_BY_EDGE >= bottom && bottom < getDocumentHeight()) {
       window.scrollBy(0, SCROLL_BY_EDGE);
     } else if (pageY - SCROLL_BY_EDGE <= top) {
       window.scrollBy(0, -SCROLL_BY_EDGE);
     }
-    if (pageX + SCROLL_BY_EDGE >= right && right < documentWidth) {
+    if (pageX + SCROLL_BY_EDGE >= right && right < getDocumentWidth()) {
       window.scrollBy(SCROLL_BY_EDGE, 0);
     } else if (pageX - SCROLL_BY_EDGE <= left) {
       window.scrollBy(-SCROLL_BY_EDGE, 0);
     }
   }
 
   /** *********************************************
    * Selection communication
--- a/browser/extensions/screenshots/webextension/sitehelper.js
+++ b/browser/extensions/screenshots/webextension/sitehelper.js
@@ -1,37 +1,75 @@
 /* globals catcher, callBackground */
 /** This is a content script added to all screenshots.firefox.com pages, and allows the site to
     communicate with the add-on */
 
 "use strict";
 
 this.sitehelper = (function() {
 
+  let ContentXMLHttpRequest = XMLHttpRequest;
+  // This gives us the content's copy of XMLHttpRequest, instead of the wrapped
+  // copy that this content script gets:
+  if (location.origin === "https://screenshots.firefox.com" ||
+      location.origin === "http://localhost:10080") {
+    // Note http://localhost:10080 is the default development server
+    // This code should always run, unless this content script is
+    // somehow run in a bad/malicious context
+    ContentXMLHttpRequest = window.wrappedJSObject.XMLHttpRequest;
+  }
+
   catcher.registerHandler((errorObj) => {
     callBackground("reportError", errorObj);
   });
 
 
   function sendCustomEvent(name, detail) {
     if (typeof detail == "object") {
       // Note sending an object can lead to security problems, while a string
       // is safe to transfer:
       detail = JSON.stringify(detail);
     }
     document.dispatchEvent(new CustomEvent(name, {detail}));
   }
 
+  /** Set the cookie, even if third-party cookies are disabled in this browser
+      (when they are disabled, login from the background page won't set cookies) */
+  function sendBackupCookieRequest(authHeaders) {
+    // See https://bugzilla.mozilla.org/show_bug.cgi?id=1295660
+    //   This bug would allow us to access window.content.XMLHttpRequest, and get
+    //   a safer (not overridable by content) version of the object.
+
+    // This is a very minimal attempt to verify that the XMLHttpRequest object we got
+    // is legitimate. It is not a good test.
+    if (Object.toString.apply(ContentXMLHttpRequest) != "function XMLHttpRequest() {\n    [native code]\n}") {
+      console.warn("Insecure copy of XMLHttpRequest");
+      return;
+    }
+    let req = new ContentXMLHttpRequest();
+    req.open("POST", "/api/set-login-cookie");
+    for (let name in authHeaders) {
+      req.setRequestHeader(name, authHeaders[name]);
+    }
+    req.send("");
+    req.onload = () => {
+      if (req.status != 200) {
+        console.warn("Attempt to set Screenshots cookie via /api/set-login-cookie failed:", req.status, req.statusText, req.responseText);
+      }
+    };
+  }
+
   document.addEventListener("delete-everything", catcher.watchFunction((event) => {
     // FIXME: reset some data in the add-on
   }, false));
 
   document.addEventListener("request-login", catcher.watchFunction((event) => {
     let shotId = event.detail;
     catcher.watchPromise(callBackground("getAuthInfo", shotId || null).then((info) => {
+      sendBackupCookieRequest(info.authHeaders);
       sendCustomEvent("login-successful", {deviceId: info.deviceId, isOwner: info.isOwner});
     }));
   }));
 
   document.addEventListener("request-onboarding", catcher.watchFunction((event) => {
     callBackground("requestOnboarding");
   }));