Bug 1390917 - Accept data:image/png and data:image/jpeg as theme background; r?aswan
MozReview-Commit-ID: 2roQoBrc7mv
--- 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",