tor-browser

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

ManifestProcessor.sys.mjs (10086B)


      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 http://mozilla.org/MPL/2.0/. */
      4 /*
      5 * ManifestProcessor
      6 * Implementation of processing algorithms from:
      7 * http://www.w3.org/2008/webapps/manifest/
      8 *
      9 * Creates manifest processor that lets you process a JSON file
     10 * or individual parts of a manifest object. A manifest is just a
     11 * standard JS object that has been cleaned up.
     12 *
     13 *   .process({jsonText,manifestURL,docURL});
     14 *
     15 * Depends on ImageObjectProcessor to process things like
     16 * icons and splash_screens.
     17 *
     18 * TODO: The constructor should accept the UA's supported orientations.
     19 * TODO: The constructor should accept the UA's supported display modes.
     20 */
     21 
     22 const displayModes = new Set([
     23  "fullscreen",
     24  "standalone",
     25  "minimal-ui",
     26  "browser",
     27 ]);
     28 const orientationTypes = new Set([
     29  "any",
     30  "natural",
     31  "landscape",
     32  "portrait",
     33  "portrait-primary",
     34  "portrait-secondary",
     35  "landscape-primary",
     36  "landscape-secondary",
     37 ]);
     38 const textDirections = new Set(["ltr", "rtl", "auto"]);
     39 
     40 // ValueExtractor is used by the various processors to get values
     41 // from the manifest and to report errors.
     42 import { ValueExtractor } from "resource://gre/modules/ValueExtractor.sys.mjs";
     43 
     44 // ImageObjectProcessor is used to process things like icons and images
     45 import { ImageObjectProcessor } from "resource://gre/modules/ImageObjectProcessor.sys.mjs";
     46 
     47 const domBundle = Services.strings.createBundle(
     48  "chrome://global/locale/dom/dom.properties"
     49 );
     50 
     51 export var ManifestProcessor = {
     52  get defaultDisplayMode() {
     53    return "browser";
     54  },
     55  get displayModes() {
     56    return displayModes;
     57  },
     58  get orientationTypes() {
     59    return orientationTypes;
     60  },
     61  get textDirections() {
     62    return textDirections;
     63  },
     64  // process() method processes JSON text into a clean manifest
     65  // that conforms with the W3C specification. Takes an object
     66  // expecting the following dictionary items:
     67  //  * jsonText: the JSON string to be processed.
     68  //  * manifestURL: the URL of the manifest, to resolve URLs.
     69  //  * docURL: the URL of the owner doc, for security checks
     70  //  * checkConformance: boolean. If true, collects any conformance
     71  //    errors into a "moz_validation" property on the returned manifest.
     72  process(aOptions) {
     73    const {
     74      jsonText,
     75      manifestURL: aManifestURL,
     76      docURL: aDocURL,
     77      checkConformance,
     78    } = aOptions;
     79 
     80    // The errors get populated by the different process* functions.
     81    const errors = [];
     82 
     83    let rawManifest = {};
     84    try {
     85      rawManifest = JSON.parse(jsonText);
     86    } catch (e) {
     87      errors.push({ type: "json", error: e.message });
     88    }
     89    if (rawManifest === null) {
     90      return null;
     91    }
     92    if (typeof rawManifest !== "object") {
     93      const warn = domBundle.GetStringFromName("ManifestShouldBeObject");
     94      errors.push({ warn });
     95      rawManifest = {};
     96    }
     97    const manifestURL = new URL(aManifestURL);
     98    const docURL = new URL(aDocURL);
     99    const extractor = new ValueExtractor(errors, domBundle);
    100    const imgObjProcessor = new ImageObjectProcessor(
    101      errors,
    102      extractor,
    103      domBundle
    104    );
    105    const processedManifest = {
    106      dir: processDirMember.call(this),
    107      lang: processLangMember(),
    108      start_url: processStartURLMember(),
    109      display: processDisplayMember.call(this),
    110      orientation: processOrientationMember.call(this),
    111      name: processNameMember(),
    112      icons: imgObjProcessor.process(rawManifest, manifestURL, "icons"),
    113      short_name: processShortNameMember(),
    114      theme_color: processThemeColorMember(),
    115      background_color: processBackgroundColorMember(),
    116    };
    117    processedManifest.scope = processScopeMember();
    118    processedManifest.id = processIdMember();
    119    if (checkConformance) {
    120      processedManifest.moz_validation = errors;
    121      processedManifest.moz_manifest_url = manifestURL.href;
    122    }
    123    return processedManifest;
    124 
    125    function processDirMember() {
    126      const spec = {
    127        objectName: "manifest",
    128        object: rawManifest,
    129        property: "dir",
    130        expectedType: "string",
    131        trim: true,
    132      };
    133      const value = extractor.extractValue(spec);
    134      if (
    135        value &&
    136        typeof value === "string" &&
    137        this.textDirections.has(value.toLowerCase())
    138      ) {
    139        return value.toLowerCase();
    140      }
    141      return "auto";
    142    }
    143 
    144    function processNameMember() {
    145      const spec = {
    146        objectName: "manifest",
    147        object: rawManifest,
    148        property: "name",
    149        expectedType: "string",
    150        trim: true,
    151      };
    152      return extractor.extractValue(spec);
    153    }
    154 
    155    function processShortNameMember() {
    156      const spec = {
    157        objectName: "manifest",
    158        object: rawManifest,
    159        property: "short_name",
    160        expectedType: "string",
    161        trim: true,
    162      };
    163      return extractor.extractValue(spec);
    164    }
    165 
    166    function processOrientationMember() {
    167      const spec = {
    168        objectName: "manifest",
    169        object: rawManifest,
    170        property: "orientation",
    171        expectedType: "string",
    172        trim: true,
    173      };
    174      const value = extractor.extractValue(spec);
    175      if (
    176        value &&
    177        typeof value === "string" &&
    178        this.orientationTypes.has(value.toLowerCase())
    179      ) {
    180        return value.toLowerCase();
    181      }
    182      return undefined;
    183    }
    184 
    185    function processDisplayMember() {
    186      const spec = {
    187        objectName: "manifest",
    188        object: rawManifest,
    189        property: "display",
    190        expectedType: "string",
    191        trim: true,
    192      };
    193      const value = extractor.extractValue(spec);
    194      if (
    195        value &&
    196        typeof value === "string" &&
    197        displayModes.has(value.toLowerCase())
    198      ) {
    199        return value.toLowerCase();
    200      }
    201      return this.defaultDisplayMode;
    202    }
    203 
    204    function processScopeMember() {
    205      const spec = {
    206        objectName: "manifest",
    207        object: rawManifest,
    208        property: "scope",
    209        expectedType: "string",
    210        trim: false,
    211      };
    212      const startURL = new URL(processedManifest.start_url);
    213      const defaultScope = new URL(".", startURL).href;
    214      const value = extractor.extractValue(spec);
    215      if (value === undefined || value === "") {
    216        return defaultScope;
    217      }
    218      let scopeURL = URL.parse(value, manifestURL);
    219      if (!scopeURL) {
    220        const warn = domBundle.GetStringFromName("ManifestScopeURLInvalid");
    221        errors.push({ warn });
    222        return defaultScope;
    223      }
    224      if (scopeURL.origin !== docURL.origin) {
    225        const warn = domBundle.GetStringFromName("ManifestScopeNotSameOrigin");
    226        errors.push({ warn });
    227        return defaultScope;
    228      }
    229      // If start URL is not within scope of scope URL:
    230      if (
    231        startURL.origin !== scopeURL.origin ||
    232        startURL.pathname.startsWith(scopeURL.pathname) === false
    233      ) {
    234        const warn = domBundle.GetStringFromName(
    235          "ManifestStartURLOutsideScope"
    236        );
    237        errors.push({ warn });
    238        return defaultScope;
    239      }
    240      // Drop search params and fragment
    241      // https://github.com/w3c/manifest/pull/961
    242      scopeURL.hash = "";
    243      scopeURL.search = "";
    244      return scopeURL.href;
    245    }
    246 
    247    function processStartURLMember() {
    248      const spec = {
    249        objectName: "manifest",
    250        object: rawManifest,
    251        property: "start_url",
    252        expectedType: "string",
    253        trim: false,
    254      };
    255      const defaultStartURL = new URL(docURL).href;
    256      const value = extractor.extractValue(spec);
    257      if (value === undefined || value === "") {
    258        return defaultStartURL;
    259      }
    260      let potentialResult = URL.parse(value, manifestURL);
    261      if (!potentialResult) {
    262        const warn = domBundle.GetStringFromName("ManifestStartURLInvalid");
    263        errors.push({ warn });
    264        return defaultStartURL;
    265      }
    266      if (potentialResult.origin !== docURL.origin) {
    267        const warn = domBundle.GetStringFromName(
    268          "ManifestStartURLShouldBeSameOrigin"
    269        );
    270        errors.push({ warn });
    271        return defaultStartURL;
    272      }
    273      return potentialResult.href;
    274    }
    275 
    276    function processThemeColorMember() {
    277      const spec = {
    278        objectName: "manifest",
    279        object: rawManifest,
    280        property: "theme_color",
    281        expectedType: "string",
    282        trim: true,
    283      };
    284      return extractor.extractColorValue(spec);
    285    }
    286 
    287    function processBackgroundColorMember() {
    288      const spec = {
    289        objectName: "manifest",
    290        object: rawManifest,
    291        property: "background_color",
    292        expectedType: "string",
    293        trim: true,
    294      };
    295      return extractor.extractColorValue(spec);
    296    }
    297 
    298    function processLangMember() {
    299      const spec = {
    300        objectName: "manifest",
    301        object: rawManifest,
    302        property: "lang",
    303        expectedType: "string",
    304        trim: true,
    305      };
    306      return extractor.extractLanguageValue(spec);
    307    }
    308 
    309    function processIdMember() {
    310      // the start_url serves as the fallback, in case the id is not specified
    311      // or in error. A start_url is assured.
    312      const startURL = new URL(processedManifest.start_url);
    313 
    314      const spec = {
    315        objectName: "manifest",
    316        object: rawManifest,
    317        property: "id",
    318        expectedType: "string",
    319        trim: false,
    320      };
    321      const extractedValue = extractor.extractValue(spec);
    322 
    323      if (typeof extractedValue !== "string" || extractedValue === "") {
    324        return startURL.href;
    325      }
    326 
    327      let appId = URL.parse(extractedValue, startURL.origin);
    328      if (!appId) {
    329        const warn = domBundle.GetStringFromName("ManifestIdIsInvalid");
    330        errors.push({ warn });
    331        return startURL.href;
    332      }
    333 
    334      if (appId.origin !== startURL.origin) {
    335        const warn = domBundle.GetStringFromName("ManifestIdNotSameOrigin");
    336        errors.push({ warn });
    337        return startURL.href;
    338      }
    339 
    340      return appId.href;
    341    }
    342  },
    343 };