tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ImageObjectProcessor.sys.mjs (6698B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 /*
      5 * ImageObjectProcessor
      6 * Implementation of Image Object processing algorithms from:
      7 * http://www.w3.org/TR/appmanifest/#image-object-and-its-members
      8 *
      9 * This is intended to be used in conjunction with ManifestProcessor.sys.mjs
     10 *
     11 * Creates an object to process Image Objects as defined by the
     12 * W3C specification. This is used to process things like the
     13 * icon member and the splash_screen member.
     14 *
     15 * Usage:
     16 *
     17 *   .process(aManifest, aBaseURL, aMemberName);
     18 *
     19 */
     20 
     21 export function ImageObjectProcessor(aErrors, aExtractor, aBundle) {
     22  this.errors = aErrors;
     23  this.extractor = aExtractor;
     24  this.domBundle = aBundle;
     25 }
     26 
     27 const iconPurposes = Object.freeze(["any", "maskable", "monochrome"]);
     28 
     29 // Static getters
     30 Object.defineProperties(ImageObjectProcessor, {
     31  decimals: {
     32    get() {
     33      return /^\d+$/;
     34    },
     35  },
     36  anyRegEx: {
     37    get() {
     38      return new RegExp("any", "i");
     39    },
     40  },
     41 });
     42 
     43 ImageObjectProcessor.prototype.process = function (
     44  aManifest,
     45  aBaseURL,
     46  aMemberName
     47 ) {
     48  const spec = {
     49    objectName: "manifest",
     50    object: aManifest,
     51    property: aMemberName,
     52    expectedType: "array",
     53    trim: false,
     54  };
     55  const { domBundle, extractor, errors } = this;
     56  const images = [];
     57  const value = extractor.extractValue(spec);
     58  if (Array.isArray(value)) {
     59    value
     60      .map(toImageObject)
     61      // Filter out images that resulted in "failure", per spec.
     62      .filter(image => image)
     63      .forEach(image => images.push(image));
     64  }
     65  return images;
     66 
     67  function toImageObject(aImageSpec, index) {
     68    let img; // if "failure" happens below, we return undefined.
     69    try {
     70      // can throw
     71      const src = processSrcMember(aImageSpec, aBaseURL, index);
     72      // can throw
     73      const purpose = processPurposeMember(aImageSpec, index);
     74      const type = processTypeMember(aImageSpec);
     75      const sizes = processSizesMember(aImageSpec);
     76      img = {
     77        src,
     78        purpose,
     79        type,
     80        sizes,
     81      };
     82    } catch (err) {
     83      /* Errors are collected by each process* function */
     84    }
     85    return img;
     86  }
     87 
     88  function processPurposeMember(aImage, index) {
     89    const spec = {
     90      objectName: "image",
     91      object: aImage,
     92      property: "purpose",
     93      expectedType: "string",
     94      trim: true,
     95      throwTypeError: true,
     96    };
     97 
     98    // Type errors are treated at "any"...
     99    let value;
    100    try {
    101      value = extractor.extractValue(spec);
    102    } catch (err) {
    103      return ["any"];
    104    }
    105 
    106    // Was only whitespace...
    107    if (!value) {
    108      return ["any"];
    109    }
    110 
    111    const keywords = value.split(/\s+/);
    112 
    113    // Emtpy is treated as "any"...
    114    if (keywords.length === 0) {
    115      return ["any"];
    116    }
    117 
    118    // We iterate over keywords and classify them into:
    119    const purposes = new Set();
    120    const unknownPurposes = new Set();
    121    const repeatedPurposes = new Set();
    122 
    123    for (const keyword of keywords) {
    124      const canonicalKeyword = keyword.toLowerCase();
    125 
    126      if (purposes.has(canonicalKeyword)) {
    127        repeatedPurposes.add(keyword);
    128        continue;
    129      }
    130 
    131      iconPurposes.includes(canonicalKeyword)
    132        ? purposes.add(canonicalKeyword)
    133        : unknownPurposes.add(keyword);
    134    }
    135 
    136    // Tell developer about unknown purposes...
    137    if (unknownPurposes.size) {
    138      const warn = domBundle.formatStringFromName(
    139        "ManifestImageUnsupportedPurposes",
    140        [aMemberName, index, [...unknownPurposes].join(" ")]
    141      );
    142      errors.push({ warn });
    143    }
    144 
    145    // Tell developer about repeated purposes...
    146    if (repeatedPurposes.size) {
    147      const warn = domBundle.formatStringFromName(
    148        "ManifestImageRepeatedPurposes",
    149        [aMemberName, index, [...repeatedPurposes].join(" ")]
    150      );
    151      errors.push({ warn });
    152    }
    153 
    154    if (purposes.size === 0) {
    155      const warn = domBundle.formatStringFromName("ManifestImageUnusable", [
    156        aMemberName,
    157        index,
    158      ]);
    159      errors.push({ warn });
    160      throw new TypeError(warn);
    161    }
    162 
    163    return [...purposes];
    164  }
    165 
    166  function processTypeMember(aImage) {
    167    const charset = {};
    168    const hadCharset = {};
    169    const spec = {
    170      objectName: "image",
    171      object: aImage,
    172      property: "type",
    173      expectedType: "string",
    174      trim: true,
    175    };
    176    let value = extractor.extractValue(spec);
    177    if (value) {
    178      value = Services.io.parseRequestContentType(value, charset, hadCharset);
    179    }
    180    return value || undefined;
    181  }
    182 
    183  function processSrcMember(aImage, aBaseURL, index) {
    184    const spec = {
    185      objectName: aMemberName,
    186      object: aImage,
    187      property: "src",
    188      expectedType: "string",
    189      trim: false,
    190      throwTypeError: true,
    191    };
    192    const value = extractor.extractValue(spec);
    193    let url;
    194    if (typeof value === "undefined" || value === "") {
    195      // We throw here as the value is unusable,
    196      // but it's not an developer error.
    197      throw new TypeError();
    198    }
    199    if (value && value.length) {
    200      try {
    201        url = new URL(value, aBaseURL).href;
    202      } catch (e) {
    203        const warn = domBundle.formatStringFromName(
    204          "ManifestImageURLIsInvalid",
    205          [aMemberName, index, "src", value]
    206        );
    207        errors.push({ warn });
    208        throw e;
    209      }
    210    }
    211    return url;
    212  }
    213 
    214  function processSizesMember(aImage) {
    215    const sizes = new Set();
    216    const spec = {
    217      objectName: "image",
    218      object: aImage,
    219      property: "sizes",
    220      expectedType: "string",
    221      trim: true,
    222    };
    223    const value = extractor.extractValue(spec);
    224    if (value) {
    225      // Split on whitespace and filter out invalid values.
    226      value
    227        .split(/\s+/)
    228        .filter(isValidSizeValue)
    229        .reduce((collector, size) => collector.add(size), sizes);
    230    }
    231    return sizes.size ? Array.from(sizes) : undefined;
    232    // Implementation of HTML's link@size attribute checker.
    233    function isValidSizeValue(aSize) {
    234      const size = aSize.toLowerCase();
    235      if (ImageObjectProcessor.anyRegEx.test(aSize)) {
    236        return true;
    237      }
    238      if (!size.includes("x") || size.indexOf("x") !== size.lastIndexOf("x")) {
    239        return false;
    240      }
    241      // Split left of x for width, after x for height.
    242      const widthAndHeight = size.split("x");
    243      const w = widthAndHeight.shift();
    244      const h = widthAndHeight.join("x");
    245      const validStarts = !w.startsWith("0") && !h.startsWith("0");
    246      const validDecimals = ImageObjectProcessor.decimals.test(w + h);
    247      return validStarts && validDecimals;
    248    }
    249  }
    250 };