_validate()

in src/parsers/manifestjson.js [415:811]


  _validate() {
    // Not all messages returned by the schema are fatal to Firefox, messages
    // that are just warnings should be added to this array.
    const warnings = [
      messages.MANIFEST_PERMISSIONS.code,
      messages.MANIFEST_OPTIONAL_PERMISSIONS.code,
      messages.MANIFEST_HOST_PERMISSIONS.code,
      messages.MANIFEST_PERMISSION_UNSUPPORTED,
      messages.MANIFEST_FIELD_UNSUPPORTED,
    ];

    // Message with the following codes will be:
    //
    // - omitted if the add-on is being explicitly validated as privileged
    //   when the `--privileged` cli option was passed or `privileged` is
    //   set to true in the linter config. This is the case for privileged
    //   extensions in the https://github.com/mozilla-extensions/ org.
    //
    // - reported as warnings if the add-on is already signed
    //   (because it is expected for addons signed as privileged to be
    //   submitted to AMO to become listed, and so the warning is meant to
    //   be just informative and to let extension developers and reviewers
    //   to know that the extension is expected to be signed as privileged
    //   or it wouldn't work).
    //
    // - reported as errors if the add-on isn't signed, which should
    //   reject the submission of a privileged extension on AMO (and
    //   have it signed with a non privileged certificate by mistake).
    const privilegedManifestMessages = [
      messages.MANIFEST_PERMISSIONS_PRIVILEGED,
      messages.MANIFEST_FIELD_PRIVILEGED,
    ];

    if (this.isAlreadySigned) {
      warnings.push(...privilegedManifestMessages);
    }

    let validate = validateAddon;

    if (this.isStaticTheme) {
      validate = validateStaticTheme;
    } else if (this.isLanguagePack) {
      validate = validateLangPack;
    } else if (this.isDictionary) {
      validate = validateDictionary;
    }

    this.isValid = validate(this.parsedJSON, this.schemaValidatorOptions);

    if (!this.isValid) {
      log.debug(
        'Schema Validation messages',
        JSON.stringify(validate.errors, null, 2)
      );

      validate.errors.forEach((error) => {
        const message = this.errorLookup(error);

        // errorLookup call returned a null or undefined message,
        // and so the error should be ignored.
        if (!message) {
          return;
        }

        if (warnings.includes(message.code)) {
          this.collector.addWarning(message);
        } else {
          this.collector.addError(message);
        }

        // Add-ons with bad permissions will fail to install in Firefox, so
        // we consider them invalid.
        if (message.code === messages.MANIFEST_BAD_PERMISSION.code) {
          this.isValid = false;
        }
      });
    }

    if (this.parsedJSON.applications?.gecko_android) {
      this.collector.addError(
        messages.manifestFieldUnsupported('/applications/gecko_android')
      );
      this.isValid = false;
    }

    if (this.parsedJSON.manifest_version < 3) {
      if (
        this.parsedJSON.browser_specific_settings?.gecko &&
        this.parsedJSON.applications
      ) {
        this.collector.addWarning(messages.IGNORED_APPLICATIONS_PROPERTY);
      } else if (this.parsedJSON.applications) {
        this.collector.addWarning(messages.APPLICATIONS_DEPRECATED);
      }
    }

    if (
      this.parsedJSON.browser_specific_settings &&
      (this.parsedJSON.browser_specific_settings.gecko ||
        this.parsedJSON.browser_specific_settings.gecko_android)
    ) {
      this.parsedJSON.applications = {
        ...(this.parsedJSON.applications || {}),
        ...this.parsedJSON.browser_specific_settings,
      };
    }

    // We only want `admin_install_only` to be set in `bss` when `--enterprise`
    // is set, otherwise we don't want the flag _at all_, which includes both
    // `bss` and `applications`.
    if (this.isEnterpriseAddon) {
      if (
        this.parsedJSON.browser_specific_settings?.gecko?.admin_install_only !==
        true
      ) {
        this.collector.addError(messages.ADMIN_INSTALL_ONLY_REQUIRED);
        this.isValid = false;
      }
    } else if (
      typeof this.parsedJSON.applications?.gecko?.admin_install_only !==
      'undefined'
    ) {
      this.collector.addError(messages.ADMIN_INSTALL_ONLY_PROP_RESERVED);
      this.isValid = false;
    }

    if (
      this.parsedJSON.browser_specific_settings?.gecko
        ?.data_collection_permissions
    ) {
      this.collector.addError(
        messages.DATA_COLLECTION_PERMISSIONS_PROP_RESERVED
      );
      this.isValid = false;
    }

    if (this.parsedJSON.content_security_policy != null) {
      this.validateCspPolicy(this.parsedJSON.content_security_policy);
    }

    if (this.parsedJSON.update_url) {
      this.collector.addNotice(messages.MANIFEST_UNUSED_UPDATE);
    }

    if (this.parsedJSON.granted_host_permissions) {
      this.collector.addWarning(
        messages.manifestFieldPrivilegedOnly('granted_host_permissions')
      );
    }

    if (this.parsedJSON.background) {
      const hasScripts = Array.isArray(this.parsedJSON.background.scripts);
      if (hasScripts) {
        this.parsedJSON.background.scripts.forEach((script) => {
          this.validateFileExistsInPackage(script, 'script');
        });
      }

      const hasPage = !!this.parsedJSON.background.page;
      if (hasPage) {
        this.validateFileExistsInPackage(
          this.parsedJSON.background.page,
          'page'
        );
      }

      if (this.parsedJSON.background.service_worker) {
        if (!this.schemaValidatorOptions?.enableBackgroundServiceWorker) {
          // Report an error and mark the manifest as invalid if background
          // service worker support isn't enabled by the addons-linter feature
          // flag.
          if (hasScripts || hasPage) {
            this.collector.addWarning(
              messages.manifestFieldUnsupported('/background/service_worker')
            );
          } else {
            this.collector.addError(
              messages.manifestFieldUnsupported('/background/service_worker')
            );
            this.isValid = false;
          }
        } else if (this.parsedJSON.manifest_version >= 3) {
          this.validateFileExistsInPackage(
            this.parsedJSON.background.service_worker,
            'script'
          );
        }
      }
    }

    if (
      this.parsedJSON.content_scripts &&
      this.parsedJSON.content_scripts.length
    ) {
      this.parsedJSON.content_scripts.forEach((scriptRule) => {
        if (scriptRule.matches && scriptRule.matches.length) {
          // Since `include_globs` only get's checked for patterns that are in
          // `matches` we only need to validate `matches`
          scriptRule.matches.forEach((matchPattern) => {
            this.validateContentScriptMatchPattern(matchPattern);
          });
        }

        if (scriptRule.js && scriptRule.js.length) {
          scriptRule.js.forEach((script) => {
            this.validateFileExistsInPackage(
              script,
              'script',
              messages.manifestContentScriptFileMissing
            );
          });
        }
        if (scriptRule.css && scriptRule.css.length) {
          scriptRule.css.forEach((style) => {
            this.validateFileExistsInPackage(
              style,
              'css',
              messages.manifestContentScriptFileMissing
            );
          });
        }
      });
    }

    if (this.parsedJSON.dictionaries) {
      if (!this.getAddonId()) {
        this.collector.addError(messages.MANIFEST_DICT_MISSING_ID);
        this.isValid = false;
      }
      const numberOfDictionaries = Object.keys(
        this.parsedJSON.dictionaries
      ).length;
      if (numberOfDictionaries < 1) {
        this.collector.addError(messages.MANIFEST_EMPTY_DICTS);
        this.isValid = false;
      } else if (numberOfDictionaries > 1) {
        this.collector.addError(messages.MANIFEST_MULTIPLE_DICTS);
        this.isValid = false;
      }
      Object.keys(this.parsedJSON.dictionaries).forEach((locale) => {
        const filepath = this.parsedJSON.dictionaries[locale];
        this.validateFileExistsInPackage(
          filepath,
          'binary',
          messages.manifestDictionaryFileMissing
        );
        // A corresponding .aff file should exist for every .dic.
        this.validateFileExistsInPackage(
          filepath.replace(/\.dic$/, '.aff'),
          'binary',
          messages.manifestDictionaryFileMissing
        );
      });
    }

    if (
      (!this.selfHosted || this.isEnterpriseAddon) &&
      this.parsedJSON.applications?.gecko?.update_url
    ) {
      if (this.isPrivilegedAddon) {
        // We cannot know whether a privileged add-on will be listed or
        // unlisted so we only emit a warning for MANIFEST_UPDATE_URL (not an
        // error).
        this.collector.addWarning(messages.MANIFEST_UPDATE_URL);
      } else {
        this.collector.addError(messages.MANIFEST_UPDATE_URL);
        this.isValid = false;
      }
    }

    if (
      !this.isLanguagePack &&
      (this.parsedJSON.applications?.gecko?.strict_max_version ||
        this.parsedJSON.applications?.gecko_android?.strict_max_version)
    ) {
      if (this.isDictionary) {
        // Dictionaries should not have a strict_max_version at all.
        this.isValid = false;
        this.collector.addError(messages.STRICT_MAX_VERSION);
      } else {
        // Rest of the extensions can, even though it's not recommended.
        this.collector.addNotice(messages.STRICT_MAX_VERSION);
      }
    }

    const minFirefoxVersion = firefoxStrictMinVersion(this.parsedJSON);
    const minAndroidVersion = androidStrictMinVersion(this.parsedJSON);
    if (
      !this.isLanguagePack &&
      !this.isDictionary &&
      (minFirefoxVersion || minAndroidVersion)
    ) {
      for (const key in bcd.webextensions.manifest) {
        if (Object.prototype.hasOwnProperty.call(this.parsedJSON, key)) {
          const compatInfo = bcd.webextensions.manifest[key];
          this.checkCompatInfo(
            compatInfo,
            minFirefoxVersion,
            minAndroidVersion,
            key,
            this.parsedJSON[key]
          );
        }
      }
    }

    this.validateName();
    this.validateVersionString();

    if (this.parsedJSON.default_locale) {
      const msg = path.join(
        LOCALES_DIRECTORY,
        this.parsedJSON.default_locale,
        'messages.json'
      );

      // Convert filename to unix path separator before
      // searching it into the scanned files map.
      if (!this.io.files[upath.toUnix(msg)]) {
        this.collector.addError(messages.NO_MESSAGES_FILE);
        this.isValid = false;
      }
    }

    if (this?.io?.files) {
      const fileList = Object.keys(this.io.files);
      const localeDirRe = new RegExp(`^${LOCALES_DIRECTORY}/(.*?)/`);
      const localeFileRe = new RegExp(
        `^${LOCALES_DIRECTORY}/.*?/${MESSAGES_JSON}$`
      );

      const locales = [];
      const localesWithMessagesJson = [];
      const errors = [];

      // Collect distinct locales (based on the content of `_locales/`) as
      // well as the locales for which we have a `messages.json` file.
      for (let i = 0; i < fileList.length; i++) {
        const matches = fileList[i].match(localeDirRe);

        if (matches && !locales.includes(matches[1])) {
          locales.push(matches[1]);
        }

        if (matches && fileList[i].match(localeFileRe)) {
          localesWithMessagesJson.push(matches[1]);
        }
      }

      // Emit an error for each locale without a `messages.json` file.
      for (let i = 0; i < locales.length; i++) {
        if (!localesWithMessagesJson.includes(locales[i])) {
          errors.push(
            messages.noMessagesFileInLocales(
              path.join(LOCALES_DIRECTORY, locales[i])
            )
          );
        }
      }

      // When there is no default locale, we do not want to emit errors for
      // missing locale files because we ignore those files.
      if (!this.parsedJSON.default_locale) {
        if (localesWithMessagesJson.length) {
          this.collector.addError(messages.NO_DEFAULT_LOCALE);
          this.isValid = false;
        }
      } else if (errors.length > 0) {
        for (const error of errors) {
          this.collector.addError(error);
        }
        this.isValid = false;
      }
    }

    if (this.parsedJSON.developer) {
      const { name, url } = this.parsedJSON.developer;

      if (name) {
        this.parsedJSON.author = name;
      }

      if (url) {
        this.parsedJSON.homepage_url = url;
      }
    }

    if (this.parsedJSON.homepage_url) {
      this.validateHomePageURL(this.parsedJSON.homepage_url);
    }

    this.validateRestrictedPermissions();
    this.validateExtensionID();
    this.validateHiddenAddon();
    this.validateDeprecatedBrowserStyle();
    this.validateIncognito();
  }