Bug 1559412 - Support web app manifest ImageResource.purpose r=baku

implementation of purpose member

Differential Revision: https://phabricator.services.mozilla.com/D38627

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Marcos Cáceres 2019-08-15 12:55:35 +00:00
Родитель e99e0cc6db
Коммит 26bc3a959f
7 изменённых файлов: 289 добавлений и 16 удалений

Просмотреть файл

@ -250,6 +250,12 @@ ManifestInvalidCSSColor=%1$S: %2$S is not a valid CSS color.
ManifestLangIsInvalid=%1$S: %2$S is not a valid language code.
# LOCALIZATION NOTE: %1$S is the name of the parent property whose value is invalid (e.g., "icons"). %2$S is the index of the image object that is invalid (from 0). %3$S is the name of actual member that is invalid. %4$S is the invalid value. E.g. "icons item at index 2 is invalid. The src member is an invalid URL http://:Invalid"
ManifestImageURLIsInvalid=%1$S item at index %2$S is invalid. The %3$S member is an invalid URL %4$S
# LOCALIZATION NOTE: %1$S is the name of the parent property that that contains the unusable image object (e.g., "icons"). %2$S is the index of the image object that is unusable (from 0). E.g. "icons item at index 2 lacks a usable purpose. It will be ignored."
ManifestImageUnusable=%1$S item at index %2$S lacks a usable purpose. It will be ignored.
# LOCALIZATION NOTE: %1$S is the name of the parent property that contains the unsupported value (e.g., "icons"). %2$S is the index of the image object that has the unsupported value (from 0). %3$S are the unknown purposes. E.g. "icons item at index 2 includes unsupported purpose(s): a b."
ManifestImageUnsupportedPurposes=%1$S item at index %2$S includes unsupported purpose(s): %3$S.
# LOCALIZATION NOTE: %1$S is the name of the parent property that has a repeated purpose (e.g., "icons"). %2$S is the index of the image object that has the repeated purpose (from 0). %3$S is the repeated purposes. E.g. "icons item at index 2 includes repeated purpose(s): a b."
ManifestImageRepeatedPurposes=%1$S item at index %2$S includes repeated purpose(s): %3$S.
PatternAttributeCompileFailure=Unable to check <input pattern='%S'> because the pattern is not a valid regexp: %S
# LOCALIZATION NOTE: Do not translate "postMessage" or DOMWindow. %S values are origins, like https://domain.com:port
TargetPrincipalDoesNotMatch=Failed to execute postMessage on DOMWindow: The target origin provided (%S) does not match the recipient windows origin (%S).

Просмотреть файл

@ -34,6 +34,8 @@ function ImageObjectProcessor(aErrors, aExtractor, aBundle) {
this.domBundle = aBundle;
}
const iconPurposes = Object.freeze(["any", "maskable"]);
// Static getters
Object.defineProperties(ImageObjectProcessor, {
decimals: {
@ -64,20 +66,111 @@ ImageObjectProcessor.prototype.process = function(
const images = [];
const value = extractor.extractValue(spec);
if (Array.isArray(value)) {
// Filter out images whose "src" is not useful.
value
.filter((item, index) => !!processSrcMember(item, aBaseURL, index))
.map(toImageObject)
// Filter out images that resulted in "failure", per spec.
.filter(image => image)
.forEach(image => images.push(image));
}
return images;
function toImageObject(aImageSpec) {
return {
src: processSrcMember(aImageSpec, aBaseURL),
type: processTypeMember(aImageSpec),
sizes: processSizesMember(aImageSpec),
function toImageObject(aImageSpec, index) {
let img; // if "failure" happens below, we return undefined.
try {
// can throw
const src = processSrcMember(aImageSpec, aBaseURL, index);
// can throw
const purpose = processPurposeMember(aImageSpec, index);
const type = processTypeMember(aImageSpec);
const sizes = processSizesMember(aImageSpec);
img = {
src,
purpose,
type,
sizes,
};
} catch (err) {
/* Errors are collected by each process* function */
}
return img;
}
function processPurposeMember(aImage, index) {
const spec = {
objectName: "image",
object: aImage,
property: "purpose",
expectedType: "string",
trim: true,
throwTypeError: true,
};
// Type errors are treated at "any"...
let value;
try {
value = extractor.extractValue(spec);
} catch (err) {
return ["any"];
}
// Was only whitespace...
if (!value) {
return ["any"];
}
const keywords = value.split(/\s+/);
// Emtpy is treated as "any"...
if (keywords.length === 0) {
return ["any"];
}
// We iterate over keywords and classify them into:
const purposes = new Set();
const unknownPurposes = new Set();
const repeatedPurposes = new Set();
for (const keyword of keywords) {
const canonicalKeyword = keyword.toLowerCase();
if (purposes.has(canonicalKeyword)) {
repeatedPurposes.add(keyword);
continue;
}
iconPurposes.includes(canonicalKeyword)
? purposes.add(canonicalKeyword)
: unknownPurposes.add(keyword);
}
// Tell developer about unknown purposes...
if (unknownPurposes.size) {
const warn = domBundle.formatStringFromName(
"ManifestImageUnsupportedPurposes",
[aMemberName, index, [...unknownPurposes].join(" ")]
);
errors.push({ warn });
}
// Tell developer about repeated purposes...
if (repeatedPurposes.size) {
const warn = domBundle.formatStringFromName(
"ManifestImageRepeatedPurposes",
[aMemberName, index, [...repeatedPurposes].join(" ")]
);
errors.push({ warn });
}
if (purposes.size === 0) {
const warn = domBundle.formatStringFromName("ManifestImageUnusable", [
aMemberName,
index,
]);
errors.push({ warn });
throw new TypeError(warn);
}
return [...purposes];
}
function processTypeMember(aImage) {
@ -108,9 +201,15 @@ ImageObjectProcessor.prototype.process = function(
property: "src",
expectedType: "string",
trim: false,
throwTypeError: true,
};
const value = extractor.extractValue(spec);
let url;
if (typeof value === "undefined" || value === "") {
// We throw here as the value is unusable,
// but it's not an developer error.
throw new TypeError();
}
if (value && value.length) {
try {
url = new URL(value, aBaseURL).href;
@ -120,6 +219,7 @@ ImageObjectProcessor.prototype.process = function(
[aMemberName, index, "src", value]
);
errors.push({ warn });
throw e;
}
}
return url;

Просмотреть файл

@ -69,14 +69,18 @@ var ManifestObtainer = {
* Adds proprietary moz_* members to manifest.
* @return {Promise<Object>} The processed manifest.
*/
contentObtainManifest(aContent, aOptions = { checkConformance: false }) {
async contentObtainManifest(
aContent,
aOptions = { checkConformance: false }
) {
if (!aContent || isXULBrowser(aContent)) {
const err = new TypeError("Invalid input. Expected a DOM Window.");
return Promise.reject(err);
}
return fetchManifest(aContent).then(response =>
processResponse(response, aContent, aOptions)
);
const response = await fetchManifest(aContent);
const result = await processResponse(response, aContent, aOptions);
const clone = Cu.cloneInto(result, aContent);
return clone;
},
};

Просмотреть файл

@ -28,7 +28,16 @@ ValueExtractor.prototype = {
// objectName: string used to construct the developer warning.
// property: the name of the property being extracted.
// trim: boolean, if the value should be trimmed (used by string type).
extractValue({ expectedType, object, objectName, property, trim }) {
// throwTypeError: boolean, throw a TypeError if the type is incorrect.
extractValue(options) {
const {
expectedType,
object,
objectName,
property,
throwTypeError,
trim,
} = options;
const value = object[property];
const isArray = Array.isArray(value);
// We need to special-case "array", as it's not a JS primitive.
@ -40,6 +49,9 @@ ValueExtractor.prototype = {
[objectName, property, expectedType]
);
this.errors.push({ warn });
if (throwTypeError) {
throw new TypeError(warn);
}
}
return undefined;
}

Просмотреть файл

@ -5,6 +5,7 @@ support-files =
manifestLoader.html
file_reg_appinstalled_event.html
file_testserver.sjs
[test_ImageObjectProcessor_purpose.html]
[test_ImageObjectProcessor_sizes.html]
[test_ImageObjectProcessor_src.html]
[test_ImageObjectProcessor_type.html]

Просмотреть файл

@ -0,0 +1,100 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=
-->
<head>
<meta charset="utf-8">
<title>Test for Bug </title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
<script src="common.js"></script>
<script>
/**
* Image object's purpose member
* https://w3c.github.io/manifest/#purpose-member
**/
"use strict";
const testManifest = {
icons: [{
src: "test",
}],
};
const invalidPurposeTypes = [
[],
123,
{},
null,
]
invalidPurposeTypes.forEach(invalidType => {
const expected = `Invalid types get treated as 'any'.`;
testManifest.icons[0].purpose = invalidType;
data.jsonText = JSON.stringify(testManifest);
const result = processor.process(data);
is(result.icons.length, 1, expected);
is(result.icons[0].purpose.length, 1, expected);
is(result.icons[0].purpose[0], "any", expected);
});
const invalidPurposes = [
"not-known-test-purpose",
"invalid-purpose invalid-purpose",
"no-purpose invalid-purpose some-other-non-valid-purpose",
];
invalidPurposes.forEach(invalidPurpose => {
const expected = `Expect invalid purposes to invalidate the icon.`;
testManifest.icons[0].purpose = invalidPurpose;
data.jsonText = JSON.stringify(testManifest);
const result = processor.process(data);
is(result.icons.length, 0, expected);
});
const mixedValidAndInvalidPurposes = [
"not-known-test-purpose maskable",
"maskable invalid-purpose invalid-purpose",
"no-purpose invalid-purpose maskable some-other-non-valid-purpose",
];
mixedValidAndInvalidPurposes.forEach(mixedPurpose => {
const expected = `Expect on 'maskable' to remain.`;
testManifest.icons[0].purpose = mixedPurpose;
data.jsonText = JSON.stringify(testManifest);
const result = processor.process(data);
is(result.icons.length, 1, expected);
is(result.icons[0].purpose.join(), "maskable", expected);
});
const validPurposes = [
"maskable",
"any",
"any maskable",
"maskable any",
];
validPurposes.forEach(purpose => {
testManifest.icons[0].purpose = purpose;
data.jsonText = JSON.stringify(testManifest);
var manifest = processor.process(data);
is(manifest.icons[0].purpose.join(" "), purpose, `Expected "${purpose}" as purpose.`);
});
const validWhiteSpace = [
"",
whiteSpace, // defined in common.js
`${whiteSpace}any`,
`any${whiteSpace}`,
`${whiteSpace}any${whiteSpace}`,
];
validWhiteSpace.forEach(purpose => {
testManifest.icons[0].purpose = purpose;
data.jsonText = JSON.stringify(testManifest);
var manifest = processor.process(data);
is(manifest.icons[0].purpose.join(), "any");
});
</script>
</head>

Просмотреть файл

@ -61,18 +61,68 @@ const options = {...data, checkConformance: true } ;
{
func: () => options.jsonText = JSON.stringify({
icons: [
{ "src": "http://exmaple.com", "sizes": "48x48"},
{ "src": "http://example.com", "sizes": "48x48"},
{ "src": "http://:Invalid", "sizes": "48x48"},
],
}),
warn: "icons item at index 1 is invalid. The src member is an invalid URL http://:Invalid",
},
// testing dom.properties: ManifestImageUnusable
{
func() {
return (options.jsonText = JSON.stringify({
icons: [
{ src: "http://example.com", purpose: "any" }, // valid
{ src: "http://example.com", purpose: "banana" }, // generates error
],
}));
},
get warn() {
// Returns 2 warnings... array here is just to keep them organized
return [
"icons item at index 1 includes unsupported purpose(s): banana.",
"icons item at index 1 lacks a usable purpose. It will be ignored.",
].join(" ");
},
},
// testing dom.properties: ManifestImageUnsupportedPurposes
{
func() {
return (options.jsonText = JSON.stringify({
icons: [
{ src: "http://example.com", purpose: "any" }, // valid
{ src: "http://example.com", purpose: "any foo bar baz bar bar baz" }, // generates error
],
}));
},
warn: "icons item at index 1 includes unsupported purpose(s): foo bar baz.",
},
// testing dom.properties: ManifestImageRepeatedPurposes
{
func() {
return (options.jsonText = JSON.stringify({
icons: [
{ src: "http://example.com", purpose: "any" }, // valid
{
src: "http://example.com",
purpose: "any maskable any maskable maskable", // generates error
},
],
}));
},
warn: "icons item at index 1 includes repeated purpose(s): any maskable.",
},
].forEach((test, index) => {
test.func();
const result = processor.process(options);
const [message] = result.moz_validation;
is(message.warn, test.warn, "Check warning.");
options.manifestURL = manifestURL;
let messages = [];
// Poking directly at "warn" triggers xray security wrapper.
for (const validationError of result.moz_validation) {
const { warn } = validationError;
messages.push(warn);
}
is(messages.join(" "), test.warn, "Check warning.");
options.manifestURL = manifestURL;
options.docURL = docURL;
});