Bug 1295607 - Avoid CSP errors when drawing the window into the eyedropper. r=miker, a=ritu
authorPatrick Brosset <pbrosset@mozilla.com>
Thu, 18 Aug 2016 14:37:04 +0200
changeset 347954 06a16c8ce5546898a555b971ba291d3b87414c5b
parent 347953 145d2272fe844b59a4b07d836273e0cef8bf31dd
child 347955 2031351aaad91af6de7d332f05af8c135d243054
push id6389
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:38:22 +0000
treeherdermozilla-beta@01d67bfe6c81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmiker, ritu
bugs1295607
milestone50.0a2
Bug 1295607 - Avoid CSP errors when drawing the window into the eyedropper. r=miker, a=ritu Pages defining CSP response headers used to be a problem for the eyedropper. Indeed, the eyedropper would take a screenshot of the window with canvas.drawWindow and then load the resulting data as an Image. But in order to access the Image() constructor, it would use the content window: new window.Image(), and that wasn't possible with CSP headers. With this change, the eyedropper creates an ImageBitmap with window.createImageBitmap() and that doesn't cause CSP errors, and still works fine because ImageBitmap are consumable by the eyedropper. This change also adds a new test to prevent this bug from coming back. MozReview-Commit-ID: 7f3HCXJtTiv
devtools/client/inspector/test/browser.ini
devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js
devtools/client/inspector/test/doc_inspector_csp.html
devtools/client/inspector/test/doc_inspector_csp.html^headers^
devtools/client/inspector/test/head.js
devtools/server/actors/highlighters/eye-dropper.js
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -1,14 +1,16 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   doc_inspector_add_node.html
   doc_inspector_breadcrumbs.html
+  doc_inspector_csp.html
+  doc_inspector_csp.html^headers^
   doc_inspector_delete-selected-node-01.html
   doc_inspector_delete-selected-node-02.html
   doc_inspector_embed.html
   doc_inspector_gcli-inspect-command.html
   doc_inspector_highlight_after_transition.html
   doc_inspector_highlighter-comments.html
   doc_inspector_highlighter-geometry_01.html
   doc_inspector_highlighter-geometry_02.html
@@ -60,16 +62,17 @@ skip-if = os == "mac" # Full keyboard na
 [browser_inspector_highlighter-04.js]
 [browser_inspector_highlighter-by-type.js]
 [browser_inspector_highlighter-comments.js]
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
 [browser_inspector_highlighter-embed.js]
 [browser_inspector_highlighter-eyedropper-clipboard.js]
 subsuite = clipboard
+[browser_inspector_highlighter-eyedropper-csp.js]
 [browser_inspector_highlighter-eyedropper-events.js]
 [browser_inspector_highlighter-eyedropper-label.js]
 [browser_inspector_highlighter-eyedropper-show-hide.js]
 [browser_inspector_highlighter-geometry_01.js]
 [browser_inspector_highlighter-geometry_02.js]
 [browser_inspector_highlighter-geometry_03.js]
 [browser_inspector_highlighter-geometry_04.js]
 [browser_inspector_highlighter-geometry_05.js]
--- a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
@@ -9,17 +9,18 @@ const HIGHLIGHTER_TYPE = "EyeDropper";
 const ID = "eye-dropper-";
 const TEST_URI = "data:text/html;charset=utf-8,<style>html{background:red}</style>";
 
 add_task(function* () {
   let helper = yield openInspectorForURL(TEST_URI)
                .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
   helper.prefix = ID;
 
-  let {show, synthesizeKey, finalize} = helper;
+  let {show, synthesizeKey, finalize,
+       waitForElementAttributeSet, waitForElementAttributeRemoved} = helper;
 
   info("Show the eyedropper with the copyOnSelect option");
   yield show("html", {copyOnSelect: true});
 
   info("Make sure to wait until the eyedropper is done taking a screenshot of the page");
   yield waitForElementAttributeSet("root", "drawn", helper);
 
   yield waitForClipboard(() => {
@@ -32,34 +33,8 @@ add_task(function* () {
 
   yield waitForElementAttributeRemoved("root", "drawn", helper);
   yield waitForElementAttributeSet("root", "hidden", helper);
   ok(true, "The eyedropper is now hidden");
 
   finalize();
 });
 
-function* waitForElementAttributeSet(id, name, {getElementAttribute}) {
-  yield poll(function* () {
-    let value = yield getElementAttribute(id, name);
-    return !!value;
-  }, `Waiting for element ${id} to have attribute ${name} set`);
-}
-
-function* waitForElementAttributeRemoved(id, name, {getElementAttribute}) {
-  yield poll(function* () {
-    let value = yield getElementAttribute(id, name);
-    return !value;
-  }, `Waiting for element ${id} to have attribute ${name} removed`);
-}
-
-function* poll(check, desc) {
-  info(desc);
-
-  for (let i = 0; i < 10; i++) {
-    if (yield check()) {
-      return;
-    }
-    yield new Promise(resolve => setTimeout(resolve, 200));
-  }
-
-  throw new Error(`Timeout while: ${desc}`);
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-csp.js
@@ -0,0 +1,30 @@
+/* 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";
+
+// Test that the eyedropper opens correctly even when the page defines CSP headers.
+
+const HIGHLIGHTER_TYPE = "EyeDropper";
+const ID = "eye-dropper-";
+const TEST_URI = URL_ROOT + "doc_inspector_csp.html";
+
+add_task(function* () {
+  let helper = yield openInspectorForURL(TEST_URI)
+               .then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
+  helper.prefix = ID;
+  let {show, hide, finalize, isElementHidden, waitForElementAttributeSet} = helper;
+
+  info("Try to display the eyedropper");
+  yield show("html");
+
+  let hidden = yield isElementHidden("root");
+  ok(!hidden, "The eyedropper is now shown");
+
+  info("Wait until the eyedropper is done taking a screenshot of the page");
+  yield waitForElementAttributeSet("root", "drawn", helper);
+  ok(true, "The image data was retrieved successfully from the window");
+
+  yield hide();
+  finalize();
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_csp.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Inspector CSP Test</title>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    This HTTP response has CSP headers.
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/doc_inspector_csp.html^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/html; charset=UTF-8
+content-security-policy: default-src 'self'; connect-src 'self'; script-src 'self'; style-src 'self';
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -390,16 +390,38 @@ function* getNodeFrontForSelector(select
   }
 
   info("Retrieving front for doctype node");
   let {nodes} = yield inspector.walker.children(inspector.walker.rootNode);
   return nodes[0];
 }
 
 /**
+ * A simple polling helper that executes a given function until it returns true.
+ * @param {Function} check A generator function that is expected to return true at some
+ * stage.
+ * @param {String} desc A text description to be displayed when the polling starts.
+ * @param {Number} attemptes Optional number of times we poll. Defaults to 10.
+ * @param {Number} timeBetweenAttempts Optional time to wait between each attempt.
+ * Defaults to 200ms.
+ */
+function* poll(check, desc, attempts = 10, timeBetweenAttempts = 200) {
+  info(desc);
+
+  for (let i = 0; i < attempts; i++) {
+    if (yield check()) {
+      return;
+    }
+    yield new Promise(resolve => setTimeout(resolve, timeBetweenAttempts));
+  }
+
+  throw new Error(`Timeout while: ${desc}`);
+}
+
+/**
  * Encapsulate some common operations for highlighter's tests, to have
  * the tests cleaner, without exposing directly `inspector`, `highlighter`, and
  * `testActor` if not needed.
  *
  * @param  {String}
  *    The highlighter's type
  * @return
  *    A generator function that takes an object with `inspector` and `testActor`
@@ -455,16 +477,32 @@ const getHighlighterHelperFor = (type) =
           prefix + id, highlighter);
       },
 
       getElementAttribute: function* (id, name) {
         return yield testActor.getHighlighterNodeAttribute(
           prefix + id, name, highlighter);
       },
 
+      waitForElementAttributeSet: function* (id, name) {
+        yield poll(function* () {
+          let value = yield testActor.getHighlighterNodeAttribute(
+            prefix + id, name, highlighter);
+          return !!value;
+        }, `Waiting for element ${id} to have attribute ${name} set`);
+      },
+
+      waitForElementAttributeRemoved: function* (id, name) {
+        yield poll(function* () {
+          let value = yield testActor.getHighlighterNodeAttribute(
+            prefix + id, name, highlighter);
+          return !value;
+        }, `Waiting for element ${id} to have attribute ${name} removed`);
+      },
+
       synthesizeMouse: function* (options) {
         options = Object.assign({selector: ":root"}, options);
         yield testActor.synthesizeMouse(options);
       },
 
       synthesizeKey: function* (options) {
         yield testActor.synthesizeKey(options);
       },
--- a/devtools/server/actors/highlighters/eye-dropper.js
+++ b/devtools/server/actors/highlighters/eye-dropper.js
@@ -178,31 +178,31 @@ EyeDropper.prototype = {
 
     this.getElement("root").setAttribute("hidden", "true");
     this.getElement("root").removeAttribute("drawn");
 
     this.emit("hidden");
   },
 
   prepareImageCapture() {
-    // Get the page as an image.
+    // Get the image data from the content window.
     let imageData = getWindowAsImageData(this.win);
-    let image = new this.win.Image();
-    image.src = imageData;
 
-    // Wait for screenshot to load
-    image.onload = () => {
+    // We need to transform imageData to something drawWindow will consume. An ImageBitmap
+    // works well. We could have used an Image, but doing so results in errors if the page
+    // defines CSP headers.
+    this.win.createImageBitmap(imageData).then(image => {
       this.pageImage = image;
       // We likely haven't drawn anything yet (no mousemove events yet), so start now.
       this.draw();
 
       // Set an attribute on the root element to be able to run tests after the first draw
       // was done.
       this.getElement("root").setAttribute("drawn", "true");
-    };
+    });
   },
 
   /**
    * Get the number of cells (blown-up pixels) per direction in the grid.
    */
   get cellsWide() {
     // Canvas will render whole "pixels" (cells) only, and an even number at that. Round
     // up to the nearest even number of pixels.
@@ -452,35 +452,35 @@ EyeDropper.prototype = {
       this._copyTimeout = setTimeout(resolve, CLOSE_DELAY);
     });
   }
 };
 
 exports.EyeDropper = EyeDropper;
 
 /**
- * Get a content window as image data-url.
+ * Draw the visible portion of the window on a canvas and get the resulting ImageData.
  * @param {Window} win
- * @return {String} The data-url
+ * @return {ImageData} The image data for the window.
  */
 function getWindowAsImageData(win) {
   let canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
   let scale = getCurrentZoom(win);
   let width = win.innerWidth;
   let height = win.innerHeight;
   canvas.width = width * scale;
   canvas.height = height * scale;
   canvas.mozOpaque = true;
 
   let ctx = canvas.getContext("2d");
 
   ctx.scale(scale, scale);
   ctx.drawWindow(win, win.scrollX, win.scrollY, width, height, "#fff");
 
-  return canvas.toDataURL();
+  return ctx.getImageData(0, 0, canvas.width, canvas.height);
 }
 
 /**
  * Get a formatted CSS color string from a color value.
  * @param {array} rgb Rgb values of a color to format.
  * @param {string} format Format of string. One of "hex", "rgb", "hsl", "name".
  * @return {string} Formatted color value, e.g. "#FFF" or "hsl(20, 10%, 10%)".
  */