forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			261 lines
		
	
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			261 lines
		
	
	
	
		
			6.9 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 https://mozilla.org/MPL/2.0/. */
 | |
| /*
 | |
|  * ImageObjectProcessor
 | |
|  * Implementation of Image Object processing algorithms from:
 | |
|  * http://www.w3.org/TR/appmanifest/#image-object-and-its-members
 | |
|  *
 | |
|  * This is intended to be used in conjunction with ManifestProcessor.jsm
 | |
|  *
 | |
|  * Creates an object to process Image Objects as defined by the
 | |
|  * W3C specification. This is used to process things like the
 | |
|  * icon member and the splash_screen member.
 | |
|  *
 | |
|  * Usage:
 | |
|  *
 | |
|  *   .process(aManifest, aBaseURL, aMemberName);
 | |
|  *
 | |
|  */
 | |
| /* exported EXPORTED_SYMBOLS*/
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const { XPCOMUtils } = ChromeUtils.import(
 | |
|   "resource://gre/modules/XPCOMUtils.jsm"
 | |
| );
 | |
| const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
 | |
| 
 | |
| function ImageObjectProcessor(aErrors, aExtractor, aBundle) {
 | |
|   this.errors = aErrors;
 | |
|   this.extractor = aExtractor;
 | |
|   this.domBundle = aBundle;
 | |
| }
 | |
| 
 | |
| const iconPurposes = Object.freeze(["any", "maskable", "monochrome"]);
 | |
| 
 | |
| // Static getters
 | |
| Object.defineProperties(ImageObjectProcessor, {
 | |
|   decimals: {
 | |
|     get() {
 | |
|       return /^\d+$/;
 | |
|     },
 | |
|   },
 | |
|   anyRegEx: {
 | |
|     get() {
 | |
|       return new RegExp("any", "i");
 | |
|     },
 | |
|   },
 | |
| });
 | |
| 
 | |
| ImageObjectProcessor.prototype.process = function(
 | |
|   aManifest,
 | |
|   aBaseURL,
 | |
|   aMemberName
 | |
| ) {
 | |
|   const spec = {
 | |
|     objectName: "manifest",
 | |
|     object: aManifest,
 | |
|     property: aMemberName,
 | |
|     expectedType: "array",
 | |
|     trim: false,
 | |
|   };
 | |
|   const { domBundle, extractor, errors } = this;
 | |
|   const images = [];
 | |
|   const value = extractor.extractValue(spec);
 | |
|   if (Array.isArray(value)) {
 | |
|     value
 | |
|       .map(toImageObject)
 | |
|       // Filter out images that resulted in "failure", per spec.
 | |
|       .filter(image => image)
 | |
|       .forEach(image => images.push(image));
 | |
|   }
 | |
|   return images;
 | |
| 
 | |
|   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) {
 | |
|     const charset = {};
 | |
|     const hadCharset = {};
 | |
|     const spec = {
 | |
|       objectName: "image",
 | |
|       object: aImage,
 | |
|       property: "type",
 | |
|       expectedType: "string",
 | |
|       trim: true,
 | |
|     };
 | |
|     let value = extractor.extractValue(spec);
 | |
|     if (value) {
 | |
|       value = Services.io.parseRequestContentType(value, charset, hadCharset);
 | |
|     }
 | |
|     return value || undefined;
 | |
|   }
 | |
| 
 | |
|   function processSrcMember(aImage, aBaseURL, index) {
 | |
|     const spec = {
 | |
|       objectName: aMemberName,
 | |
|       object: aImage,
 | |
|       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;
 | |
|       } catch (e) {
 | |
|         const warn = domBundle.formatStringFromName(
 | |
|           "ManifestImageURLIsInvalid",
 | |
|           [aMemberName, index, "src", value]
 | |
|         );
 | |
|         errors.push({ warn });
 | |
|         throw e;
 | |
|       }
 | |
|     }
 | |
|     return url;
 | |
|   }
 | |
| 
 | |
|   function processSizesMember(aImage) {
 | |
|     const sizes = new Set();
 | |
|     const spec = {
 | |
|       objectName: "image",
 | |
|       object: aImage,
 | |
|       property: "sizes",
 | |
|       expectedType: "string",
 | |
|       trim: true,
 | |
|     };
 | |
|     const value = extractor.extractValue(spec);
 | |
|     if (value) {
 | |
|       // Split on whitespace and filter out invalid values.
 | |
|       value
 | |
|         .split(/\s+/)
 | |
|         .filter(isValidSizeValue)
 | |
|         .reduce((collector, size) => collector.add(size), sizes);
 | |
|     }
 | |
|     return sizes.size ? Array.from(sizes) : undefined;
 | |
|     // Implementation of HTML's link@size attribute checker.
 | |
|     function isValidSizeValue(aSize) {
 | |
|       const size = aSize.toLowerCase();
 | |
|       if (ImageObjectProcessor.anyRegEx.test(aSize)) {
 | |
|         return true;
 | |
|       }
 | |
|       if (!size.includes("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 = ImageObjectProcessor.decimals.test(w + h);
 | |
|       return validStarts && validDecimals;
 | |
|     }
 | |
|   }
 | |
| };
 | |
| var EXPORTED_SYMBOLS = ["ImageObjectProcessor"]; // jshint ignore:line
 | 
