Bug 1390917 - Accept data:image/png and data:image/jpeg as theme background; r?aswan draft
authorAndreas Wagner <mail@andreaswagner.org>
Thu, 17 Aug 2017 21:51:36 +0200
changeset 654300 419d983e7c3635242d15ace551b467de80ae3335
parent 654299 e3b8f85ad0049a8ff809d1f5672182196f186d2c
child 728527 fd05e10570522ed9b13757114079a0d9066257d6
push id76530
push userawagner@mozilla.com
push dateMon, 28 Aug 2017 14:13:41 +0000
reviewersaswan
bugs1390917
milestone57.0a1
Bug 1390917 - Accept data:image/png and data:image/jpeg as theme background; r?aswan MozReview-Commit-ID: 2roQoBrc7mv
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/schemas/manifest.json
toolkit/components/extensions/schemas/theme.json
toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js
toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -928,16 +928,29 @@ const FORMATS = {
       } catch (e) {
         return FORMATS.relativeUrl(string, context);
       }
     }
 
     throw new SyntaxError(`String ${JSON.stringify(string)} must be a relative URL`);
   },
 
+  imageDataOrStrictRelativeUrl(string, context) {
+    // Do not accept a string which resolves as an absolute URL, or any
+    // protocol-relative URL, except PNG or JPG data URLs
+    if (!string.startsWith("data:image/png;base64,") && !string.startsWith("data:image/jpeg;base64,")) {
+      try {
+        return FORMATS.strictRelativeUrl(string, context);
+      } catch (e) {
+        throw new SyntaxError(`String ${JSON.stringify(string)} must be a relative or PNG or JPG data:image URL`);
+      }
+    }
+    return string;
+  },
+
   contentSecurityPolicy(string, context) {
     let error = contentPolicyService.validateAddonCSP(string);
     if (error != null) {
       throw new SyntaxError(error);
     }
     return string;
   },
 
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -267,16 +267,21 @@
         "pattern": "^https?://.*$"
       },
       {
         "id": "ExtensionURL",
         "type": "string",
         "format": "strictRelativeUrl"
       },
       {
+        "id": "ImageDataOrExtensionURL",
+        "type": "string",
+        "format": "imageDataOrStrictRelativeUrl"
+      },
+      {
         "id": "ExtensionID",
         "choices": [
           {
             "type": "string",
             "pattern": "(?i)^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}$"
           },
           {
             "type": "string",
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -20,25 +20,25 @@
         "type": "object",
         "properties": {
           "images": {
             "type": "object",
             "optional": true,
             "properties": {
               "additional_backgrounds": {
                 "type": "array",
-                "items": { "$ref": "ExtensionURL" },
+                "items": { "$ref": "ImageDataOrExtensionURL" },
                 "optional": true
               },
               "headerURL": {
-                "$ref": "ExtensionURL",
+                "$ref": "ImageDataOrExtensionURL",
                 "optional": true
               },
               "theme_frame": {
-                "$ref": "ExtensionURL",
+                "$ref": "ImageDataOrExtensionURL",
                 "optional": true
               }
             },
             "additionalProperties": { "$ref": "UnrecognizedProperty" }
           },
           "colors": {
             "type": "object",
             "optional": true,
--- a/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js
@@ -104,8 +104,72 @@ add_task(async function test_dynamic_the
   let {backgroundImage, backgroundColor, color} = defaultStyle;
   validateTheme(backgroundImage, backgroundColor, color, false);
 
   await extension.unload();
 
   let docEl = window.document.documentElement;
   Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
 });
+
+add_task(async function test_dynamic_theme_updates_with_data_url() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["theme"],
+    },
+    background() {
+      browser.test.onMessage.addListener((msg, details) => {
+        if (msg === "update-theme") {
+          browser.theme.update(details).then(() => {
+            browser.test.sendMessage("theme-updated");
+          });
+        } else {
+          browser.theme.reset().then(() => {
+            browser.test.sendMessage("theme-reset");
+          });
+        }
+      });
+    },
+  });
+
+  let defaultStyle = window.getComputedStyle(window.document.documentElement);
+  await extension.startup();
+
+  extension.sendMessage("update-theme", {
+    "images": {
+      "headerURL": BACKGROUND_1,
+    },
+    "colors": {
+      "accentcolor": ACCENT_COLOR_1,
+      "textcolor": TEXT_COLOR_1,
+    },
+  });
+
+  await extension.awaitMessage("theme-updated");
+
+  validateTheme(BACKGROUND_1, ACCENT_COLOR_1, TEXT_COLOR_1, true);
+
+  extension.sendMessage("update-theme", {
+    "images": {
+      "headerURL": BACKGROUND_2,
+    },
+    "colors": {
+      "accentcolor": ACCENT_COLOR_2,
+      "textcolor": TEXT_COLOR_2,
+    },
+  });
+
+  await extension.awaitMessage("theme-updated");
+
+  validateTheme(BACKGROUND_2, ACCENT_COLOR_2, TEXT_COLOR_2, true);
+
+  extension.sendMessage("reset-theme");
+
+  await extension.awaitMessage("theme-reset");
+
+  let {backgroundImage, backgroundColor, color} = defaultStyle;
+  validateTheme(backgroundImage, backgroundColor, color, false);
+
+  await extension.unload();
+
+  let docEl = window.document.documentElement;
+  Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -212,16 +212,17 @@ let json = [
          {
            name: "arg",
            type: "object",
            properties: {
              hostname: {type: "string", "format": "hostname", "optional": true},
              url: {type: "string", "format": "url", "optional": true},
              relativeUrl: {type: "string", "format": "relativeUrl", "optional": true},
              strictRelativeUrl: {type: "string", "format": "strictRelativeUrl", "optional": true},
+             imageDataOrStrictRelativeUrl: {type: "string", "format": "imageDataOrStrictRelativeUrl", "optional": true},
            },
          },
        ],
      },
 
      {
        name: "formatDate",
        type: "function",
@@ -623,54 +624,86 @@ add_task(async function() {
   tallied = null;
 
   Assert.throws(() => root.testing.pattern("DEADcow"),
                 /String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
                 "should throw for non-match");
 
   root.testing.format({hostname: "foo"});
   verify("call", "testing", "format", [{hostname: "foo",
+                                        imageDataOrStrictRelativeUrl: null,
                                         relativeUrl: null,
                                         strictRelativeUrl: null,
                                         url: null}]);
   tallied = null;
 
   for (let invalid of ["", " ", "http://foo", "foo/bar", "foo.com/", "foo?"]) {
     Assert.throws(() => root.testing.format({hostname: invalid}),
                   /Invalid hostname/,
                   "should throw for invalid hostname");
   }
 
   root.testing.format({url: "http://foo/bar",
                        relativeUrl: "http://foo/bar"});
   verify("call", "testing", "format", [{hostname: null,
+                                        imageDataOrStrictRelativeUrl: null,
                                         relativeUrl: "http://foo/bar",
                                         strictRelativeUrl: null,
                                         url: "http://foo/bar"}]);
   tallied = null;
 
   root.testing.format({relativeUrl: "foo.html", strictRelativeUrl: "foo.html"});
   verify("call", "testing", "format", [{hostname: null,
+                                        imageDataOrStrictRelativeUrl: null,
                                         relativeUrl: `${wrapper.url}foo.html`,
                                         strictRelativeUrl: `${wrapper.url}foo.html`,
                                         url: null}]);
   tallied = null;
 
+  root.testing.format({imageDataOrStrictRelativeUrl: "data:image/png;base64,A"});
+  verify("call", "testing", "format", [{hostname: null,
+                                        imageDataOrStrictRelativeUrl: "data:image/png;base64,A",
+                                        relativeUrl: null,
+                                        strictRelativeUrl: null,
+                                        url: null}]);
+  tallied = null;
+
+  root.testing.format({imageDataOrStrictRelativeUrl: "data:image/jpeg;base64,A"});
+  verify("call", "testing", "format", [{hostname: null,
+                                        imageDataOrStrictRelativeUrl: "data:image/jpeg;base64,A",
+                                        relativeUrl: null,
+                                        strictRelativeUrl: null,
+                                        url: null}]);
+  tallied = null;
+
+  root.testing.format({imageDataOrStrictRelativeUrl: "foo.html"});
+  verify("call", "testing", "format", [{hostname: null,
+                                        imageDataOrStrictRelativeUrl: `${wrapper.url}foo.html`,
+                                        relativeUrl: null,
+                                        strictRelativeUrl: null,
+                                        url: null}]);
+
+  tallied = null;
+
   for (let format of ["url", "relativeUrl"]) {
     Assert.throws(() => root.testing.format({[format]: "chrome://foo/content/"}),
                   /Access denied/,
                   "should throw for access denied");
   }
 
   for (let urlString of ["//foo.html", "http://foo/bar.html"]) {
     Assert.throws(() => root.testing.format({strictRelativeUrl: urlString}),
                   /must be a relative URL/,
                   "should throw for non-relative URL");
   }
 
+  Assert.throws(() => root.testing.format({imageDataOrStrictRelativeUrl: "data:image/svg+xml;utf8,A"}),
+                  /must be a relative or PNG or JPG data:image URL/,
+                  "should throw for non-relative or non PNG/JPG data URL");
+
   const dates = [
     "2016-03-04",
     "2016-03-04T08:00:00Z",
     "2016-03-04T08:00:00.000Z",
     "2016-03-04T08:00:00-08:00",
     "2016-03-04T08:00:00.000-08:00",
     "2016-03-04T08:00:00+08:00",
     "2016-03-04T08:00:00.000+08:00",