mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
361 lines
10 KiB
JavaScript
361 lines
10 KiB
JavaScript
/* 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/.
|
|
*
|
|
* ManifestProcessor
|
|
* Implementation of processing algorithms from:
|
|
* http://www.w3.org/2008/webapps/manifest/
|
|
*
|
|
* Creates manifest processor that lets you process a JSON file
|
|
* or individual parts of a manifest object. A manifest is just a
|
|
* standard JS object that has been cleaned up.
|
|
*
|
|
* .process({jsonText,manifestURL,docURL});
|
|
*
|
|
* TODO: The constructor should accept the UA's supported orientations.
|
|
* TODO: The constructor should accept the UA's supported display modes.
|
|
* TODO: hook up developer tools to console. (1086997).
|
|
*/
|
|
/*exported EXPORTED_SYMBOLS */
|
|
/*JSLint options in comment below: */
|
|
/*globals Components, XPCOMUtils*/
|
|
'use strict';
|
|
this.EXPORTED_SYMBOLS = ['ManifestProcessor'];
|
|
const imports = {};
|
|
const {
|
|
utils: Cu,
|
|
classes: Cc,
|
|
interfaces: Ci
|
|
} = Components;
|
|
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
|
|
Cu.importGlobalProperties(['URL']);
|
|
XPCOMUtils.defineLazyModuleGetter(imports, 'Services',
|
|
'resource://gre/modules/Services.jsm');
|
|
imports.netutil = Cc['@mozilla.org/network/util;1'].getService(Ci.nsINetUtil);
|
|
// Helper function extracts values from manifest members
|
|
// and reports conformance violations.
|
|
function extractValue({
|
|
objectName,
|
|
object,
|
|
property,
|
|
expectedType,
|
|
trim
|
|
}, console) {
|
|
const value = object[property];
|
|
const isArray = Array.isArray(value);
|
|
// We need to special-case "array", as it's not a JS primitive.
|
|
const type = (isArray) ? 'array' : typeof value;
|
|
if (type !== expectedType) {
|
|
if (type !== 'undefined') {
|
|
let msg = `Expected the ${objectName}'s ${property} `;
|
|
msg += `member to a be a ${expectedType}.`;
|
|
console.log(msg);
|
|
}
|
|
return undefined;
|
|
}
|
|
// Trim string and returned undefined if the empty string.
|
|
const shouldTrim = expectedType === 'string' && value && trim;
|
|
if (shouldTrim) {
|
|
return value.trim() || undefined;
|
|
}
|
|
return value;
|
|
}
|
|
const displayModes = new Set(['fullscreen', 'standalone', 'minimal-ui',
|
|
'browser'
|
|
]);
|
|
const orientationTypes = new Set(['any', 'natural', 'landscape', 'portrait',
|
|
'portrait-primary', 'portrait-secondary', 'landscape-primary',
|
|
'landscape-secondary'
|
|
]);
|
|
const {
|
|
ConsoleAPI
|
|
} = Cu.import('resource://gre/modules/devtools/Console.jsm');
|
|
|
|
class ManifestProcessor {
|
|
|
|
constructor() {}
|
|
|
|
static get defaultDisplayMode() {
|
|
return 'browser';
|
|
}
|
|
|
|
static get displayModes() {
|
|
return displayModes;
|
|
}
|
|
|
|
static get orientationTypes() {
|
|
return orientationTypes;
|
|
}
|
|
|
|
// process method: processes json text into a clean manifest
|
|
// that conforms with the W3C specification. Takes an object
|
|
// expecting the following dictionary items:
|
|
// * jsonText: the JSON string to be processd.
|
|
// * manifestURL: the URL of the manifest, to resolve URLs.
|
|
// * docURL: the URL of the owner doc, for security checks.
|
|
process({
|
|
jsonText, manifestURL, docURL
|
|
}) {
|
|
const console = new ConsoleAPI({
|
|
prefix: 'Web Manifest: '
|
|
});
|
|
let rawManifest = {};
|
|
try {
|
|
rawManifest = JSON.parse(jsonText);
|
|
} catch (e) {}
|
|
if (typeof rawManifest !== 'object' || rawManifest === null) {
|
|
let msg = 'Manifest needs to be an object.';
|
|
console.warn(msg);
|
|
rawManifest = {};
|
|
}
|
|
const processedManifest = {
|
|
start_url: processStartURLMember(rawManifest, manifestURL, docURL),
|
|
display: processDisplayMember(rawManifest),
|
|
orientation: processOrientationMember(rawManifest),
|
|
name: processNameMember(rawManifest),
|
|
icons: IconsProcessor.process(rawManifest, manifestURL, console),
|
|
short_name: processShortNameMember(rawManifest),
|
|
};
|
|
processedManifest.scope = processScopeMember(rawManifest, manifestURL,
|
|
docURL, processedManifest.start_url);
|
|
return processedManifest;
|
|
|
|
function processNameMember(aManifest) {
|
|
const spec = {
|
|
objectName: 'manifest',
|
|
object: aManifest,
|
|
property: 'name',
|
|
expectedType: 'string',
|
|
trim: true
|
|
};
|
|
return extractValue(spec, console);
|
|
}
|
|
|
|
function processShortNameMember(aManifest) {
|
|
const spec = {
|
|
objectName: 'manifest',
|
|
object: aManifest,
|
|
property: 'short_name',
|
|
expectedType: 'string',
|
|
trim: true
|
|
};
|
|
return extractValue(spec, console);
|
|
}
|
|
|
|
function processOrientationMember(aManifest) {
|
|
const spec = {
|
|
objectName: 'manifest',
|
|
object: aManifest,
|
|
property: 'orientation',
|
|
expectedType: 'string',
|
|
trim: true
|
|
};
|
|
const value = extractValue(spec, console);
|
|
if (ManifestProcessor.orientationTypes.has(value)) {
|
|
return value;
|
|
}
|
|
// The spec special-cases orientation to return the empty string.
|
|
return '';
|
|
}
|
|
|
|
function processDisplayMember(aManifest) {
|
|
const spec = {
|
|
objectName: 'manifest',
|
|
object: aManifest,
|
|
property: 'display',
|
|
expectedType: 'string',
|
|
trim: true
|
|
};
|
|
const value = extractValue(spec, console);
|
|
if (ManifestProcessor.displayModes.has(value)) {
|
|
return value;
|
|
}
|
|
return ManifestProcessor.defaultDisplayMode;
|
|
}
|
|
|
|
function processScopeMember(aManifest, aManifestURL, aDocURL, aStartURL) {
|
|
const spec = {
|
|
objectName: 'manifest',
|
|
object: aManifest,
|
|
property: 'scope',
|
|
expectedType: 'string',
|
|
trim: false
|
|
};
|
|
const value = extractValue(spec, console);
|
|
let scopeURL;
|
|
try {
|
|
scopeURL = new URL(value, aManifestURL);
|
|
} catch (e) {
|
|
let msg = 'The URL of scope is invalid.';
|
|
console.warn(msg);
|
|
return undefined;
|
|
}
|
|
if (scopeURL.origin !== aDocURL.origin) {
|
|
let msg = 'Scope needs to be same-origin as Document.';
|
|
console.warn(msg);
|
|
return undefined;
|
|
}
|
|
// If start URL is not within scope of scope URL:
|
|
let isSameOrigin = aStartURL && aStartURL.origin !== scopeURL.origin;
|
|
if (isSameOrigin || !aStartURL.pathname.startsWith(scopeURL.pathname)) {
|
|
let msg =
|
|
'The start URL is outside the scope, so scope is invalid.';
|
|
console.warn(msg);
|
|
return undefined;
|
|
}
|
|
return scopeURL;
|
|
}
|
|
|
|
function processStartURLMember(aManifest, aManifestURL, aDocURL) {
|
|
const spec = {
|
|
objectName: 'manifest',
|
|
object: aManifest,
|
|
property: 'start_url',
|
|
expectedType: 'string',
|
|
trim: false
|
|
};
|
|
let result = new URL(aDocURL);
|
|
const value = extractValue(spec, console);
|
|
if (value === undefined || value === '') {
|
|
return result;
|
|
}
|
|
let potentialResult;
|
|
try {
|
|
potentialResult = new URL(value, aManifestURL);
|
|
} catch (e) {
|
|
console.warn('Invalid URL.');
|
|
return result;
|
|
}
|
|
if (potentialResult.origin !== aDocURL.origin) {
|
|
let msg = 'start_url must be same origin as document.';
|
|
console.warn(msg);
|
|
} else {
|
|
result = potentialResult;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
this.ManifestProcessor = ManifestProcessor;
|
|
|
|
class IconsProcessor {
|
|
|
|
constructor() {
|
|
throw new Error('Static use only.');
|
|
}
|
|
|
|
static get onlyDecimals() {
|
|
return /^\d+$/;
|
|
}
|
|
|
|
static get anyRegEx() {
|
|
return new RegExp('any', 'i');
|
|
}
|
|
|
|
static process(aManifest, aBaseURL, console) {
|
|
const spec = {
|
|
objectName: 'manifest',
|
|
object: aManifest,
|
|
property: 'icons',
|
|
expectedType: 'array',
|
|
trim: false
|
|
};
|
|
const icons = [];
|
|
const value = extractValue(spec, console);
|
|
if (Array.isArray(value)) {
|
|
// Filter out icons whose "src" is not useful.
|
|
value.filter(item => !!processSrcMember(item, aBaseURL))
|
|
.map(toIconObject)
|
|
.forEach(icon => icons.push(icon));
|
|
}
|
|
return icons;
|
|
|
|
function toIconObject(aIconData) {
|
|
return {
|
|
src: processSrcMember(aIconData, aBaseURL),
|
|
type: processTypeMember(aIconData),
|
|
sizes: processSizesMember(aIconData),
|
|
density: processDensityMember(aIconData)
|
|
};
|
|
}
|
|
|
|
function processTypeMember(aIcon) {
|
|
const charset = {};
|
|
const hadCharset = {};
|
|
const spec = {
|
|
objectName: 'icon',
|
|
object: aIcon,
|
|
property: 'type',
|
|
expectedType: 'string',
|
|
trim: true
|
|
};
|
|
let value = extractValue(spec, console);
|
|
if (value) {
|
|
value = imports.netutil.parseContentType(value, charset, hadCharset);
|
|
}
|
|
return value || undefined;
|
|
}
|
|
|
|
function processDensityMember(aIcon) {
|
|
const value = parseFloat(aIcon.density);
|
|
const validNum = Number.isNaN(value) || value === +Infinity || value <=
|
|
0;
|
|
return (validNum) ? 1.0 : value;
|
|
}
|
|
|
|
function processSrcMember(aIcon, aBaseURL) {
|
|
const spec = {
|
|
objectName: 'icon',
|
|
object: aIcon,
|
|
property: 'src',
|
|
expectedType: 'string',
|
|
trim: false
|
|
};
|
|
const value = extractValue(spec, console);
|
|
let url;
|
|
if (value && value.length) {
|
|
try {
|
|
url = new URL(value, aBaseURL);
|
|
} catch (e) {}
|
|
}
|
|
return url;
|
|
}
|
|
|
|
function processSizesMember(aIcon) {
|
|
const sizes = new Set(),
|
|
spec = {
|
|
objectName: 'icon',
|
|
object: aIcon,
|
|
property: 'sizes',
|
|
expectedType: 'string',
|
|
trim: true
|
|
},
|
|
value = extractValue(spec, console);
|
|
if (value) {
|
|
// Split on whitespace and filter out invalid values.
|
|
value.split(/\s+/)
|
|
.filter(isValidSizeValue)
|
|
.forEach(size => sizes.add(size));
|
|
}
|
|
return sizes;
|
|
// Implementation of HTML's link@size attribute checker.
|
|
function isValidSizeValue(aSize) {
|
|
const size = aSize.toLowerCase();
|
|
if (IconsProcessor.anyRegEx.test(aSize)) {
|
|
return true;
|
|
}
|
|
if (!size.contains('x') || size.indexOf('x') !== size.lastIndexOf('x')) {
|
|
return false;
|
|
}
|
|
// Split left of x for width, after x for height.
|
|
const widthAndHeight = size.split('x');
|
|
const w = widthAndHeight.shift();
|
|
const h = widthAndHeight.join('x');
|
|
const validStarts = !w.startsWith('0') && !h.startsWith('0');
|
|
const validDecimals = IconsProcessor.onlyDecimals.test(w + h);
|
|
return (validStarts && validDecimals);
|
|
}
|
|
}
|
|
}
|
|
}
|