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();
}