_addCustomKeywords()

in src/schema/validator.js [492:731]


  _addCustomKeywords(validator) {
    validator.removeKeyword(SCHEMA_KEYWORDS.DEPRECATED);
    validator.addKeyword({
      keyword: SCHEMA_KEYWORDS.DEPRECATED,
      validate: function validateDeprecated(
        message,
        propValue,
        schema,
        { instancePath }
      ) {
        if (
          !Object.prototype.hasOwnProperty.call(
            DEPRECATED_MANIFEST_PROPERTIES,
            instancePath
          )
        ) {
          // Do not emit errors for every deprecated property, as it may introduce
          // regressions due to unexpected new deprecation messages raised as errors,
          // better to deal with it separately.
          return true;
        }

        validateDeprecated.errors = [
          {
            keyword: SCHEMA_KEYWORDS.DEPRECATED,
            message,
          },
        ];

        return false;
      },
      errors: true,
    });

    function createManifestVersionValidateFn(keyword, condFn) {
      // function of type SchemaValidateFunction (see ajv typescript signatures).
      return function validate(
        keywordSchemaValue,
        propValue,
        schema,
        { rootData /* instancePath, parentData, parentDataProperty, */ }
      ) {
        const manifestVersion =
          (rootData && rootData.manifest_version) || MANIFEST_VERSION_DEFAULT;
        const res = condFn(keywordSchemaValue, manifestVersion);

        // If the min/max_manifest_version is set on a schema entry of type array,
        // propagate the same keyword to the `items` schema, which is needed to
        // - be able to recognize that those schema entries are also only allowed on
        //   certain manifest versions (which becomes part of the linting messages)
        // - be able to filter out the validation errors related to future (not yet
        //   supported) manifest versions if they are related to those schema entries
        //   (which happens based on the current or parent schema in the `filterErrors`
        //   helper method).
        if (schema.type === 'array') {
          // TODO(#3774): move this at "import JSONSchema data" time, and remove it from here.
          // eslint-disable-next-line no-param-reassign
          schema.items[keyword] = schema[keyword];
        }

        if (!res) {
          // If the addon manifest is out of an enum values min/max manifest version range,
          // don't report an additional validation error for the min/max_manifest_version
          // keyword validation function.
          if (schema.enum) {
            return true;
          }
          validate.errors = [
            {
              keyword,
              params: { [keyword]: keywordSchemaValue },
            },
          ];
        }
        return res;
      };
    }

    validator.addKeyword({
      keyword: SCHEMA_KEYWORDS.MAX_MANIFEST_VERSION,
      // function of type SchemaValidateFunction (see ajv typescript signatures).
      validate: createManifestVersionValidateFn(
        SCHEMA_KEYWORDS.MAX_MANIFEST_VERSION,
        (maxMV, manifestVersion) => maxMV >= manifestVersion
      ),
      errors: true,
    });

    validator.addKeyword({
      keyword: SCHEMA_KEYWORDS.MIN_MANIFEST_VERSION,
      validate: createManifestVersionValidateFn(
        SCHEMA_KEYWORDS.MIN_MANIFEST_VERSION,
        (minMV, manifestVersion) => minMV <= manifestVersion
      ),
      errors: true,
    });

    const validatePrivilegedPermissions = (keywordSchemaValue, propValue) => {
      const privilegedPermissions = this.getPrivilegedPermissionsSet(validator);
      const found = new Set();

      for (const permission of propValue) {
        if (privilegedPermissions.has(permission)) {
          found.add(permission);
        }
      }

      const hasMozillaAddonsPermission = found.has('mozillaAddons');

      // If the addon is expected to be privileged, we report a linting error if:
      // - there are no privileged permissions required
      // - and/or if the "mozillaAddons" permission isn't requested (which is going
      //   to be a mandatory requirement even if there are also other privileged
      //   permissions already required).
      if (this.isPrivilegedAddon) {
        if (found.size === 0 || !hasMozillaAddonsPermission) {
          validatePrivilegedPermissions.errors = [
            {
              keyword: SCHEMA_KEYWORDS.VALIDATE_PRIVILEGED_PERMISSIONS,
              params: {
                postprocess: keywordSchemaValue,
                privilegedPermissions: Array.from(found),
                hasMozillaAddonsPermission,
              },
            },
          ];

          return false;
        }

        return true;
      }

      if (found.size > 0) {
        validatePrivilegedPermissions.errors = [
          {
            keyword: SCHEMA_KEYWORDS.VALIDATE_PRIVILEGED_PERMISSIONS,
            params: {
              postprocess: keywordSchemaValue,
              privilegedPermissions: Array.from(found),
              hasMozillaAddonsPermission: found.has('mozillaAddons'),
            },
          },
        ];
        return false;
      }

      return true;
    };

    validator.addKeyword({
      keyword: SCHEMA_KEYWORDS.VALIDATE_PRIVILEGED_PERMISSIONS,
      validate: validatePrivilegedPermissions,
    });

    const validatePrivilegedManifestFields = (
      keywordSchemaValue,
      propValue,
      schema,
      { rootData /* instancePath, parentData, parentDataProperty, */ }
    ) => {
      const hasMozillaAddonsPermission =
        Array.isArray(rootData.permissions) &&
        rootData.permissions.includes('mozillaAddons');

      if (this.isPrivilegedAddon) {
        if (!hasMozillaAddonsPermission) {
          validatePrivilegedManifestFields.errors = [
            {
              keyword: SCHEMA_KEYWORDS.PRIVILEGED,
              params: {
                hasMozillaAddonsPermission,
              },
            },
          ];
          return false;
        }
        return true;
      }

      validatePrivilegedManifestFields.errors = [
        {
          keyword: SCHEMA_KEYWORDS.PRIVILEGED,
          params: {
            hasMozillaAddonsPermission,
          },
        },
      ];
      return false;
    };

    validator.addKeyword({
      keyword: SCHEMA_KEYWORDS.PRIVILEGED,
      validate: validatePrivilegedManifestFields,
    });

    const validateRequiredManifestBackgroundKeys = (
      keywordSchemaValue,
      propValue,
      schema,
      { rootData /* instancePath, parentData, parentDataProperty, */ }
    ) => {
      if (keywordSchemaValue !== 'checkRequiredManifestBackgroundKeys') {
        return true;
      }
      // At least one environment is required
      if (
        !propValue.page &&
        !propValue.scripts?.length &&
        !propValue.service_worker
      ) {
        // Add an error to the manifest validations.
        const serviceWorkerEnabled =
          this.enableBackgroundServiceWorker &&
          rootData.manifest_version === 3 &&
          this.allowedManifestVersionsRange.maximum >= 3;
        const message = `requires at least one of ${
          serviceWorkerEnabled ? '"service_worker", ' : ''
        }"scripts" or "page".`;
        // Report the error as coming from a `required` keyword enforcing
        // which properties are required to be part of the `background`.
        validateRequiredManifestBackgroundKeys.errors = [
          {
            keyword: SCHEMA_KEYWORDS.REQUIRED,
            message,
            params: {
              preprocess: keywordSchemaValue,
            },
          },
        ];
        return false;
      }
      return true;
    };

    validator.addKeyword({
      keyword: 'postprocess',
      validate: validateRequiredManifestBackgroundKeys,
    });
  }