addons-linter
Version:
Mozilla Add-ons Linter
1,187 lines (1,109 loc) • 912 kB
JavaScript
require("source-map-support").install();
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ([
/* 0 */
/***/ ((module, exports, __webpack_require__) => {
/* module decorator */ module = __webpack_require__.nmd(module);
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports.createInstance = createInstance;
exports["default"] = void 0;
exports.isRunFromCLI = isRunFromCLI;
var _cli = __webpack_require__(1);
var _linter = _interopRequireDefault(__webpack_require__(9));
var _logger = _interopRequireDefault(__webpack_require__(4));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function isRunFromCLI(_module = module) {
return __webpack_require__.c[__webpack_require__.s] === _module;
}
function createInstance({
config = (0, _cli.getConfig)({
useCLI: isRunFromCLI()
}).argv,
runAsBinary = false
} = {}) {
_logger.default.level = config.logLevel;
_logger.default.info('Creating new linter instance', {
config
});
// eslint-disable-next-line no-param-reassign
config.runAsBinary = runAsBinary;
return new _linter.default(config);
}
var _default = exports["default"] = {
Linter: _linter.default,
createInstance,
isRunFromCLI
};
/***/ }),
/* 1 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports.getConfig = getConfig;
exports.terminalWidth = terminalWidth;
var _yargs = _interopRequireDefault(__webpack_require__(2));
var _commonTags = __webpack_require__(3);
var _logger = _interopRequireDefault(__webpack_require__(4));
var _yargsOptions = _interopRequireDefault(__webpack_require__(6));
var _package = __webpack_require__(8);
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function terminalWidth(_process = process) {
if (_process && _process.stdout && _process.stdout.columns > 0) {
let width = _process.stdout.columns - 2;
// Terminals less than ten pixels wide seem silly.
if (width < 10) {
width = 10;
}
return width;
}
return 78;
}
function getConfig({
useCLI = true,
argv
} = {}) {
if (useCLI === false) {
_logger.default.error((0, _commonTags.oneLine)`Config requested from CLI, but not in CLI mode.
Please supply a config instead of relying on the getConfig() call.`);
throw new Error('Cannot request config from CLI in library mode');
}
// Used by test.main,js to override CLI arguments (because
// the process.argv array is controlled by jest),
// See #1762 for a rationale.
const cliArgv = argv ? (0, _yargs.default)(argv) : _yargs.default;
return cliArgv.usage(`Usage: ./$0 [options] addon-package-or-dir \n\n
Add-ons Linter (JS Edition) v${_package.version}`).options(_yargsOptions.default)
// Require one non-option.
.demand(1).help('help').alias('h', 'help').wrap(terminalWidth());
}
/***/ }),
/* 2 */
/***/ ((module) => {
module.exports = require("yargs");
/***/ }),
/* 3 */
/***/ ((module) => {
module.exports = require("common-tags");
/***/ }),
/* 4 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports.createLogger = createLogger;
exports["default"] = void 0;
var _pino = _interopRequireDefault(__webpack_require__(5));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function createLogger(_process = process) {
const level = _process.env.LOG_LEVEL || 'fatal';
return (0, _pino.default)({
name: 'AddonLinterJS',
level
}, process.stdout);
}
var _default = exports["default"] = createLogger();
/***/ }),
/* 5 */
/***/ ((module) => {
module.exports = require("pino");
/***/ }),
/* 6 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports["default"] = void 0;
exports.getDefaultConfigValue = getDefaultConfigValue;
var _const = __webpack_require__(7);
const options = {
'log-level': {
describe: 'The log-level to generate',
type: 'string',
default: _const.DEFAULT_CONFIG.logLevel,
choices: ['fatal', 'error', 'warn', 'info', 'debug', 'trace']
},
'warnings-as-errors': {
describe: 'Treat warnings as errors',
type: 'boolean',
default: _const.DEFAULT_CONFIG.warningsAsErrors
},
output: {
alias: 'o',
describe: 'The type of output to generate',
type: 'string',
default: _const.DEFAULT_CONFIG.output,
choices: ['json', 'text']
},
metadata: {
describe: 'Output only metadata as JSON',
type: 'boolean',
default: _const.DEFAULT_CONFIG.metadata
},
pretty: {
describe: 'Prettify JSON output',
type: 'boolean',
default: _const.DEFAULT_CONFIG.pretty
},
stack: {
describe: 'Show stacktraces when errors are thrown',
type: 'boolean',
default: _const.DEFAULT_CONFIG.stack
},
boring: {
describe: 'Disable colorful shell output',
type: 'boolean',
default: _const.DEFAULT_CONFIG.boring
},
enterprise: {
describe: 'Treat the input file (or directory) as an enterprise extension (implies --self-hosted)',
type: 'boolean',
default: _const.DEFAULT_CONFIG.enterprise
},
privileged: {
describe: 'Treat the input file (or directory) as a privileged extension',
type: 'boolean',
default: _const.DEFAULT_CONFIG.privileged
},
'self-hosted': {
describe: 'Disable messages related to hosting on addons.mozilla.org',
type: 'boolean',
default: _const.DEFAULT_CONFIG.selfHosted
},
'enable-background-service-worker': {
describe: 'Enable MV3 background service worker support',
type: 'boolean',
default: _const.DEFAULT_CONFIG.enableBackgroundServiceWorker
},
'min-manifest-version': {
describe: 'Set a custom minimum allowed value for the manifest_version property',
type: 'number',
default: _const.DEFAULT_CONFIG.minManifestVersion,
requiresArg: true
},
'max-manifest-version': {
describe: 'Set a custom maximum allowed value for the manifest_version property',
type: 'number',
default: _const.DEFAULT_CONFIG.maxManifestVersion,
requiresArg: true
},
'disable-linter-rules': {
describe: 'Disable list of comma separated eslint rules',
type: 'string',
requiresArg: true
},
'disable-xpi-autoclose': {
describe: 'Disable the auto-close feature when linting XPI files',
type: 'boolean',
default: _const.DEFAULT_CONFIG.disableXpiAutoclose
},
'enable-data-collection-permissions': {
describe: 'Enable data collection permissions support',
type: 'boolean',
default: _const.DEFAULT_CONFIG.enableDataCollectionPermissions
}
};
var _default = exports["default"] = options;
function getDefaultConfigValue(name) {
if (options[name] && 'default' in options[name]) {
return options[name].default;
}
return undefined;
}
/***/ }),
/* 7 */
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports.ZIP_LIB_CORRUPT_FILE_ERROR = exports.VALIDATION_WARNING = exports.VALIDATION_NOTICE = exports.VALIDATION_ERROR = exports.TEMPORARY_APIS = exports.STATIC_THEME_IMAGE_MIMES = exports.SCHEMA_KEYWORDS_CUSTOM = exports.SCHEMA_KEYWORDS = exports.RESTRICTED_PERMISSIONS = exports.RESTRICTED_HOMEPAGE_URLS = exports.RESERVED_FILENAMES = exports.PERMS_DATAPATH_REGEX = exports.PACKAGE_TYPES = exports.PACKAGE_THEME = exports.PACKAGE_SUBPACKAGE = exports.PACKAGE_SEARCHPROV = exports.PACKAGE_MULTI = exports.PACKAGE_LANGPACK = exports.PACKAGE_EXTENSION = exports.PACKAGE_DICTIONARY = exports.PACKAGE_ANY = exports.MESSAGE_TYPES = exports.MESSAGE_PLACEHOLDER_REGEXP = exports.MESSAGES_JSON = exports.MAX_FILE_SIZE_TO_PARSE_MB = exports.MANIFEST_VERSION_MIN = exports.MANIFEST_VERSION_MAX = exports.MANIFEST_VERSION_DEFAULT = exports.MANIFEST_JSON = exports.LOCALES_DIRECTORY = exports.INSTALL_ORIGINS_DATAPATH_REGEX = exports.IMAGE_FILE_EXTENSIONS = exports.HIDDEN_FILE_REGEX = exports.FLAGGED_FILE_REGEX = exports.FLAGGED_FILE_MAGIC_NUMBERS = exports.FLAGGED_FILE_EXTENSIONS = exports.FILE_EXTENSIONS_TO_MIME = exports.EXTERNAL_RULE_MAPPING = exports.ESLINT_WARNING = exports.ESLINT_TYPES = exports.ESLINT_RULE_MAPPING = exports.ESLINT_ERROR = exports.DEPRECATED_MANIFEST_PROPERTIES = exports.DEPRECATED_JAVASCRIPT_APIS = exports.DEFAULT_CONFIG = exports.CSP_KEYWORD_RE = exports.ALREADY_SIGNED_REGEX = void 0;
const ESLINT_ERROR = exports.ESLINT_ERROR = 2;
const ESLINT_WARNING = exports.ESLINT_WARNING = 1;
const NO_UNSANITIZED_OPTIONS = {
variableTracing: false,
// Disable escapers (Sanitizer.escapeHTML, escapeHTML) and unwrappers
// (Sanitizer.unwrapSafeHTML, unwrapSafeHTML) which are allowed by default by
// this plugin.
escape: {
taggedTemplates: [],
methods: []
}
};
const NO_UNSANITIZED_METHOD_CUSTOMIZATIONS = {
import: {
escape: {
methods: ['chrome.runtime.getURL', 'browser.runtime.getURL']
}
// NOTE: Alternatively using the following option would instead
// configure the plugin to consider any method call as allowed
// on dynamic import calls:
//
// objectMatches: [],
}
};
// 3rd party / eslint-internal rules
const EXTERNAL_RULE_MAPPING = exports.EXTERNAL_RULE_MAPPING = {
'no-eval': [ESLINT_WARNING, {
allowIndirect: false
}],
'no-implied-eval': ESLINT_WARNING,
'no-new-func': ESLINT_WARNING,
'no-unsanitized/method': [ESLINT_WARNING, NO_UNSANITIZED_OPTIONS, NO_UNSANITIZED_METHOD_CUSTOMIZATIONS],
'no-unsanitized/property': [ESLINT_WARNING, NO_UNSANITIZED_OPTIONS]
};
const ESLINT_RULE_MAPPING = exports.ESLINT_RULE_MAPPING = {
'global-require-arg': ESLINT_WARNING,
'no-document-write': ESLINT_WARNING,
'webextension-api': ESLINT_WARNING,
'webextension-deprecated-api': ESLINT_WARNING,
'webextension-unsupported-api': ESLINT_WARNING,
'content-scripts-file-absent': ESLINT_ERROR,
'webextension-api-compat': ESLINT_WARNING,
'webextension-api-compat-android': ESLINT_WARNING,
...EXTERNAL_RULE_MAPPING
};
const VALIDATION_ERROR = exports.VALIDATION_ERROR = 'error';
const VALIDATION_NOTICE = exports.VALIDATION_NOTICE = 'notice';
const VALIDATION_WARNING = exports.VALIDATION_WARNING = 'warning';
const ESLINT_TYPES = exports.ESLINT_TYPES = {
0: VALIDATION_NOTICE,
1: VALIDATION_WARNING,
2: VALIDATION_ERROR
};
const MESSAGE_TYPES = exports.MESSAGE_TYPES = [VALIDATION_ERROR, VALIDATION_NOTICE, VALIDATION_WARNING];
// Package type constants.
const PACKAGE_ANY = exports.PACKAGE_ANY = 0;
const PACKAGE_EXTENSION = exports.PACKAGE_EXTENSION = 1;
const PACKAGE_THEME = exports.PACKAGE_THEME = 2;
const PACKAGE_DICTIONARY = exports.PACKAGE_DICTIONARY = 3;
const PACKAGE_LANGPACK = exports.PACKAGE_LANGPACK = 4;
const PACKAGE_SEARCHPROV = exports.PACKAGE_SEARCHPROV = 5;
const PACKAGE_MULTI = exports.PACKAGE_MULTI = 1; // A multi extension is an extension
const PACKAGE_SUBPACKAGE = exports.PACKAGE_SUBPACKAGE = 7;
const PACKAGE_TYPES = exports.PACKAGE_TYPES = {
PACKAGE_ANY,
PACKAGE_EXTENSION,
PACKAGE_THEME,
PACKAGE_DICTIONARY,
PACKAGE_LANGPACK,
PACKAGE_SEARCHPROV,
PACKAGE_MULTI,
PACKAGE_SUBPACKAGE
};
const MANIFEST_JSON = exports.MANIFEST_JSON = 'manifest.json';
// The manifest_version value to use to complete the validation if an explicit one
// was missing from the extension manifest.json (but we will still be collecting the
// error for the missing manifest_version property, because it is mandatory).
const MANIFEST_VERSION_DEFAULT = exports.MANIFEST_VERSION_DEFAULT = 2;
// Default min/max_manifest_version values used for schema definitions that do not
// have an explicit one on their own.
const MANIFEST_VERSION_MIN = exports.MANIFEST_VERSION_MIN = 2;
const MANIFEST_VERSION_MAX = exports.MANIFEST_VERSION_MAX = 3;
// This is the limit in megabytes of a file we will parse (eg. CSS, JS, etc.)
// A singular CSS/JS file over 5MB seems bad and may actually be full of data
// best stored in JSON/some other data format rather than code.
// https://github.com/mozilla/addons-linter/issues/730
// We increased this limit from 2MB to 4MB as per:
// https://github.com/mozilla/addons/issues/181
// Then from 4MB to 5MB in https://github.com/mozilla/addons-linter/issues/4942
//
// We should be careful about increasing this any further.
const MAX_FILE_SIZE_TO_PARSE_MB = exports.MAX_FILE_SIZE_TO_PARSE_MB = 5;
const HIDDEN_FILE_REGEX = exports.HIDDEN_FILE_REGEX = /^__MACOSX\//;
const FLAGGED_FILE_REGEX = exports.FLAGGED_FILE_REGEX = /thumbs\.db$|\.DS_Store$|\.orig$|\.old$|~$/i;
const ALREADY_SIGNED_REGEX = exports.ALREADY_SIGNED_REGEX = /^META-INF\/manifest\.mf/;
const PERMS_DATAPATH_REGEX = exports.PERMS_DATAPATH_REGEX = /^\/(permissions|optional_permissions|host_permissions)\/([\d+])/;
const INSTALL_ORIGINS_DATAPATH_REGEX = exports.INSTALL_ORIGINS_DATAPATH_REGEX = /^\/(install_origins)\/([\d+])/;
const RESERVED_FILENAMES = exports.RESERVED_FILENAMES = ['mozilla-recommendation.json'];
const FLAGGED_FILE_EXTENSIONS = exports.FLAGGED_FILE_EXTENSIONS = ['.class', '.dll', '.dylib', '.exe', '.jar', '.sh', '.so', '.swf'];
const IMAGE_FILE_EXTENSIONS = exports.IMAGE_FILE_EXTENSIONS = ['jpg', 'jpeg', 'webp', 'gif', 'png', 'svg'];
const FILE_EXTENSIONS_TO_MIME = exports.FILE_EXTENSIONS_TO_MIME = {
svg: 'image/svg+xml',
gif: 'image/gif',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
webp: 'image/webp'
};
// Unique list of mime types for the allowed static theme images.
const STATIC_THEME_IMAGE_MIMES = exports.STATIC_THEME_IMAGE_MIMES = [...new Set(Object.values(FILE_EXTENSIONS_TO_MIME))];
// Mapping of "schema data paths" of the deprecated properties that we
// issue warnings for.
// If the value is `null` we will be using the `deprecated` message
// from the schema. Otherwise `code`, `message` and `description` will be taken
// from the object provided.
// Note that we have to use the constants name as we can't import
// the message object here.
const DEPRECATED_MANIFEST_PROPERTIES = exports.DEPRECATED_MANIFEST_PROPERTIES = {
'/theme/images/headerURL': 'MANIFEST_THEME_LWT_ALIAS',
'/theme/colors/accentcolor': 'MANIFEST_THEME_LWT_ALIAS',
'/theme/colors/textcolor': 'MANIFEST_THEME_LWT_ALIAS'
};
// Mapping of deprecated javascript apis.
// If the value is `null` we will be using the `deprecated` message
// from the schema. Otherwise `code`, `message` and `description` will be taken
// from the object provided.
// Note that we have to use the constants name as we can't import
// the message object here.
const DEPRECATED_JAVASCRIPT_APIS = exports.DEPRECATED_JAVASCRIPT_APIS = {
// These APIs were already deprecated by Chrome and Firefox never
// supported them. We do still issue deprecation warnings for them.
'app.getDetails': 'DEPRECATED_CHROME_API',
'extension.onRequest': 'DEPRECATED_CHROME_API',
'extension.onRequestExternal': 'DEPRECATED_CHROME_API',
'extension.sendRequest': 'DEPRECATED_CHROME_API',
'tabs.getAllInWindow': 'DEPRECATED_CHROME_API',
'tabs.getSelected': 'DEPRECATED_CHROME_API',
'tabs.onActiveChanged': 'DEPRECATED_CHROME_API',
'tabs.onSelectionChanged': 'DEPRECATED_CHROME_API',
'tabs.sendRequest': 'DEPRECATED_CHROME_API',
// https://github.com/mozilla/addons-linter/issues/2556
'proxy.register': 'DEPRECATED_API',
'proxy.unregister': 'DEPRECATED_API',
'proxy.onProxyError': 'DEPRECATED_API',
'proxy.registerProxyScript': 'DEPRECATED_API'
};
// A list of magic numbers that we won't allow.
const FLAGGED_FILE_MAGIC_NUMBERS = exports.FLAGGED_FILE_MAGIC_NUMBERS = [[0x4d, 0x5a],
// EXE or DLL,
[0x5a, 0x4d],
// Alternative EXE or DLL
[0x7f, 0x45, 0x4c, 0x46],
// UNIX elf
[0x23, 0x21],
// Shell script
[0xca, 0xfe, 0xba, 0xbe],
// Java + Mach-O (dylib)
[0xca, 0xfe, 0xd0, 0x0d],
// Java packed
[0x43, 0x57, 0x53] // Compressed SWF
];
// These are APIs that will cause problems when loaded temporarily
// in about:debugging.
// APIs listed here should be defined in https://mzl.la/31p4AMc
const TEMPORARY_APIS = exports.TEMPORARY_APIS = ['identity.getRedirectURL', 'storage.sync', 'storage.managed', 'runtime.onMessageExternal', 'runtime.onConnectExternal'];
// All valid CSP keywords that are options to keys like `default-src` and
// `script-src`. Used in manifest.json parser for validation.
// See https://mzl.la/2vwqbGU for more details and allowed options.
const CSP_KEYWORD_RE = exports.CSP_KEYWORD_RE = /^'(self|none|wasm-unsafe-eval)'$|^moz-extension:/;
const MESSAGES_JSON = exports.MESSAGES_JSON = 'messages.json';
const LOCALES_DIRECTORY = exports.LOCALES_DIRECTORY = '_locales';
// This is a string, since it has to be matched globally on a message string.
// This should match
// https://searchfox.org/mozilla-central/rev/3abf6fa7e2a6d9a7bfb88796141b0f012e68c2db/toolkit/components/extensions/ExtensionCommon.jsm#1711
const MESSAGE_PLACEHOLDER_REGEXP = exports.MESSAGE_PLACEHOLDER_REGEXP = '\\$([a-zA-Z0-9_@]+)\\$';
// yauzl should trow error with this message in case of corrupt zip file
const ZIP_LIB_CORRUPT_FILE_ERROR = exports.ZIP_LIB_CORRUPT_FILE_ERROR = 'End of central directory record signature not found';
// URLs in this array are restricted from being used in the manifest.json "homepage_url" prperty
const RESTRICTED_HOMEPAGE_URLS = exports.RESTRICTED_HOMEPAGE_URLS = ['addons-dev.allizom.org', 'addons.mozilla.org'];
// This map should contain entries with a permission name as key and a min
// Firefox version as value (both string values).
const RESTRICTED_PERMISSIONS = exports.RESTRICTED_PERMISSIONS = new Map([
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1733159
['proxy', '91.1.0']]);
const SCHEMA_KEYWORDS_CUSTOM = exports.SCHEMA_KEYWORDS_CUSTOM = {
MIN_MANIFEST_VERSION: 'min_manifest_version',
MAX_MANIFEST_VERSION: 'max_manifest_version',
PRIVILEGED: 'privileged',
// This custom keyword doesn't exist on the Firefox side, but it is injected into the
// schema data as part of the schema data inmport and used by the linter to hook up
// custom validation logic for privileged permissions.
VALIDATE_PRIVILEGED_PERMISSIONS: 'validatePrivilegedPermissions'
};
const SCHEMA_KEYWORDS = exports.SCHEMA_KEYWORDS = {
// Keywords defined in the JSON schema specs.
ANY_OF: 'anyOf',
DEPRECATED: 'deprecated',
REQUIRED: 'required',
TYPE: 'type',
MIN_PROPERTIES: 'minProperties',
// Non-standard JSONSchema keywords (defined and used by the Firefox and/or addons-linter).
...SCHEMA_KEYWORDS_CUSTOM
};
// Default configuration values for the linter.
const DEFAULT_CONFIG = exports.DEFAULT_CONFIG = {
logLevel: 'fatal',
warningsAsErrors: false,
output: 'text',
metadata: false,
pretty: false,
stack: false,
boring: false,
enterprise: false,
privileged: false,
selfHosted: false,
enableBackgroundServiceWorker: false,
minManifestVersion: 2,
maxManifestVersion: 3,
disableXpiAutoclose: false,
enableDataCollectionPermissions: true
};
/***/ }),
/* 8 */
/***/ ((module) => {
module.exports = /*#__PURE__*/JSON.parse('{"name":"addons-linter","version":"10.3.0","description":"Mozilla Add-ons Linter","main":"dist/addons-linter.js","bin":{"addons-linter":"bin/addons-linter"},"engines":{"node":">=20.0.0"},"browserslist":["node >=20.0.0"],"scripts":{"build":"webpack --bail --stats-error-details true --color --config webpack.config.js","eslint":"eslint bin/* scripts/* src/* && eslint --config tests/eslint.config.mjs tests/","test":"jest --runInBand --watch \'tests/.*\'","test-coverage":"jest --runInBand --coverage --watch \'tests/.*\'","test-once":"jest --runInBand","test-coverage-once":"jest --runInBand --coverage","test-ci":"npm run test-coverage-once","test-integration":"jest --runInBand --config=jest.integration.config.js","test-integration-linter":"npm run test-integration -- tests/integration/addons-linter","test-integration:production":"node tests/integration/run-as-production-env.js test-integration tests/integration/addons-linter","lint":"npm run eslint","prettier":"prettier --write \'**\'","prettier-ci":"prettier --list-different \'**\' || (echo \'\\n\\nThis failure means you did not run `npm run prettier-dev` before committing\\n\\n\' && exit 1)","prettier-dev":"pretty-quick --branch master","build-rules":"scripts/build-rules && cp node_modules/github-markdown-css/github-markdown.css docs/github-markdown.css","webext-test-functional":"scripts/webext-test-functional","smoke-test-eslint-version-conflicts":"scripts/smoke-test-eslint-version-conflicts","update-hashes":"scripts/dispensary > src/dispensary/hashes.txt"},"repository":{"type":"git","url":"git+https://github.com/mozilla/addons-linter.git"},"author":"Mozilla Add-ons Team","license":"MPL-2.0","bugs":{"url":"https://github.com/mozilla/addons-linter/issues"},"homepage":"https://github.com/mozilla/addons-linter#readme","dependencies":{"@fluent/syntax":"0.19.0","@fregante/relaxed-json":"2.0.0","@mdn/browser-compat-data":"7.3.9","addons-moz-compare":"1.3.0","addons-scanner-utils":"15.0.0","ajv":"8.18.0","chalk":"4.1.2","cheerio":"1.2.0","columnify":"1.6.0","common-tags":"1.8.2","deepmerge":"4.3.1","eslint":"9.39.4","eslint-plugin-no-unsanitized":"4.1.5","eslint-visitor-keys":"5.0.1","espree":"11.2.0","esprima":"4.0.1","fast-json-patch":"3.1.1","image-size":"2.0.2","json-merge-patch":"1.0.2","pino":"10.3.1","semver":"7.7.4","source-map-support":"0.5.21","upath":"2.0.1","yargs":"17.7.2","yauzl":"3.2.1"},"devDependencies":{"@babel/cli":"7.28.6","@babel/core":"7.29.0","@babel/eslint-parser":"7.28.6","@babel/preset-env":"7.29.2","@babel/register":"7.28.6","async":"3.2.6","babel-core":"7.0.0-bridge.0","babel-jest":"30.3.0","babel-loader":"10.1.1","comment-json":"4.6.2","eslint-config-amo":"8.0.0","eslint-plugin-amo":"3.0.0","eslint-plugin-prettier":"5.5.5","github-markdown-css":"5.9.0","globals":"17.4.0","gunzip-maybe":"1.4.2","hashish":"0.0.4","jest":"30.3.0","lodash.clonedeep":"4.5.0","lodash.ismatchwith":"4.4.0","markdown-it":"14.1.1","markdown-it-anchor":"9.2.0","markdown-it-footnote":"4.0.0","natural-compare-lite":"1.4.0","node-fetch":"2.6.11","prettier":"3.8.1","pretty-quick":"4.2.2","raw-loader":"4.0.2","replace-in-file":"8.4.0","shelljs":"0.10.0","sinon":"21.0.3","tar-fs":"3.1.2","tmp-promise":"3.0.3","webpack":"5.105.4","webpack-cli":"7.0.2","webpack-node-externals":"3.0.0","yazl":"2.5.1"}}');
/***/ }),
/* 9 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports["default"] = void 0;
var _path = _interopRequireDefault(__webpack_require__(10));
var _columnify = _interopRequireDefault(__webpack_require__(11));
var _chalk = _interopRequireDefault(__webpack_require__(12));
var _commonTags = __webpack_require__(3);
var _utils = __webpack_require__(13);
var _errors = __webpack_require__(14);
var _io = __webpack_require__(15);
var _cli = __webpack_require__(1);
var constants = _interopRequireWildcard(__webpack_require__(7));
var _libraries = __webpack_require__(16);
var messages = _interopRequireWildcard(__webpack_require__(17));
var _utils2 = __webpack_require__(19);
var _logger = _interopRequireDefault(__webpack_require__(4));
var _collector = _interopRequireDefault(__webpack_require__(28));
var _manifestjson = _interopRequireDefault(__webpack_require__(30));
var _binary = _interopRequireDefault(__webpack_require__(113));
var _filename2 = _interopRequireDefault(__webpack_require__(115));
var _html = _interopRequireDefault(__webpack_require__(116));
var _javascript = _interopRequireDefault(__webpack_require__(121));
var _json = _interopRequireDefault(__webpack_require__(137));
var _langpack = _interopRequireDefault(__webpack_require__(139));
var _miner_blocklist = __webpack_require__(144);
var _dispensary = _interopRequireDefault(__webpack_require__(145));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
class Linter {
constructor(config) {
this.config = {
...constants.DEFAULT_CONFIG,
...config
};
[this.packagePath] = this.config._;
this.io = null;
this.chalk = new _chalk.default.Instance({
enabled: !this.config.boring
});
this.collector = new _collector.default(this.config);
this.addonMetadata = null;
this.shouldScanFile = this.shouldScanFile.bind(this);
}
set config(cfg) {
this._config = cfg;
}
get config() {
return this._config;
}
validateConfig() {
const {
minManifestVersion,
maxManifestVersion
} = this.config;
if (maxManifestVersion < minManifestVersion) {
throw new _utils2.AddonsLinterUserError(_utils2.i18n.sprintf(_utils2.i18n._(`Invalid manifest version range requested:
--min-manifest-version (currently set to %(minManifestVersion)s)
should not be greater than
--max-manifest-version (currently set to %(maxManifestVersion)s).`), {
minManifestVersion,
maxManifestVersion
}));
}
}
colorize(type) {
switch (type) {
case constants.VALIDATION_ERROR:
return this.chalk.red;
case constants.VALIDATION_WARNING:
return this.chalk.yellow;
case constants.VALIDATION_NOTICE:
return this.chalk.blue;
default:
throw new Error((0, _commonTags.oneLine)`colorize passed invalid type.
Should be one of ${constants.MESSAGE_TYPES.join(', ')}`);
}
}
closeIO() {
// This is only used when `io` is valid and we disabled the auto-close
// feature.
if (this.config.disableXpiAutoclose && this.io) {
this.io.close();
}
}
handleError(err, _console = console) {
// The zip files contains invalid entries (likely path names using invalid
// characters like '\\'), the linter can inspect the package but Firefox
// would fail to load it.
if (err instanceof _errors.InvalidZipFileError) {
this.collector.addError({
...messages.INVALID_XPI_ENTRY,
message: err.message
});
this.print(_console);
return true;
}
// The zip file contains multiple entries with the exact same file name.
if (err instanceof _errors.DuplicateZipEntryError) {
this.collector.addError(messages.DUPLICATE_XPI_ENTRY);
this.print(_console);
return true;
}
// The zip file fails to open successfully, the linter can't inspect it
// at all.
if (err.message.includes(constants.ZIP_LIB_CORRUPT_FILE_ERROR)) {
this.collector.addError(messages.BAD_ZIPFILE);
this.print(_console);
return true;
}
if (this.config.stack === true) {
_console.error(err.stack);
} else {
_console.error(this.chalk.red(err.message || err));
}
this.closeIO();
return false;
}
print(_console = console) {
if (this.config.output === 'none') {
return;
}
if (this.config.output === 'json') {
_console.log(this.toJSON(this.config.pretty));
} else {
_console.log(this.textOutput());
}
}
toJSON({
input = this.output,
pretty = this.config.pretty,
_JSON = JSON
} = {}) {
const args = [input];
if (pretty === true) {
args.push(null);
args.push(4);
}
return _JSON.stringify.apply(null, args);
}
textOutput(_terminalWidth = _cli.terminalWidth) {
const maxColumns = _terminalWidth();
const out = [];
out.push(_utils2.i18n._('Validation Summary:'));
out.push('');
out.push((0, _columnify.default)(this.output.summary, {
showHeaders: false,
minWidth: 15,
maxLineWidth: maxColumns
}));
out.push('');
constants.MESSAGE_TYPES.forEach(type => {
const messageType = `${type}s`;
if (this.output[messageType].length) {
const outputConfig = {
code: {
dataTransform: value => {
return this.colorize(type)(value);
},
headingTransform: () => {
return _utils2.i18n._('Code');
},
maxWidth: 35
},
message: {
headingTransform: () => {
return _utils2.i18n._('Message');
},
maxWidth: (maxColumns - 35) * 0.25
},
description: {
headingTransform: () => {
return _utils2.i18n._('Description');
},
maxWidth: (maxColumns - 35) * 0.5
},
file: {
headingTransform: () => {
return _utils2.i18n._('File');
},
maxWidth: (maxColumns - 35) * 0.25
},
line: {
headingTransform: () => {
return _utils2.i18n._('Line');
},
maxWidth: 6
},
column: {
headingTransform: () => {
return _utils2.i18n._('Column');
},
maxWidth: 6
}
};
const outputColumns = ['code', 'message', 'description', 'file', 'line', 'column'];
// If the terminal is this small we cave and don't size things
// contextually anymore.
if (maxColumns < 60) {
delete outputColumns[outputColumns.indexOf('column')];
delete outputConfig.column;
delete outputColumns[outputColumns.indexOf('description')];
delete outputConfig.description;
delete outputColumns[outputColumns.indexOf('line')];
delete outputConfig.line;
outputConfig.message.maxWidth = 15;
outputConfig.file.maxWidth = 15;
} else if (maxColumns < 78) {
delete outputColumns[outputColumns.indexOf('description')];
delete outputConfig.description;
outputConfig.message.maxWidth = (maxColumns - 47) * 0.5;
outputConfig.file.maxWidth = (maxColumns - 35) * 0.5;
}
out.push(`${messageType.toUpperCase()}:`);
out.push('');
out.push((0, _columnify.default)(this.output[messageType], {
maxWidth: 35,
columns: outputColumns,
columnSplitter: ' ',
config: outputConfig,
maxLineWidth: maxColumns
}));
}
});
return out.join('\n');
}
get output() {
const output = {
count: this.collector.length,
summary: {},
metadata: this.addonMetadata
};
constants.MESSAGE_TYPES.forEach(type => {
const messageType = `${type}s`;
output[messageType] = this.collector[messageType];
output.summary[messageType] = this.collector[messageType].length;
});
return output;
}
async getAddonMetadata({
_log = _logger.default,
ManifestJSONParser = _manifestjson.default
} = {}) {
if (this.addonMetadata !== null) {
_log.debug('Metadata already set; returning cached metadata.');
return this.addonMetadata;
}
const files = await this.io.getFiles();
if (Object.prototype.hasOwnProperty.call(files, constants.MANIFEST_JSON)) {
_log.info('Retrieving metadata from manifest.json');
const json = await this.io.getFileAsString(constants.MANIFEST_JSON);
const manifestParser = new ManifestJSONParser(json, this.collector, {
io: this.io,
isAlreadySigned: Object.keys(files).some(filename => constants.ALREADY_SIGNED_REGEX.test(filename)),
isEnterprise: this.config.enterprise,
selfHosted: this.config.selfHosted,
schemaValidatorOptions: {
privileged: this.config.privileged,
minManifestVersion: this.config.minManifestVersion,
maxManifestVersion: this.config.maxManifestVersion,
enableBackgroundServiceWorker: this.config.enableBackgroundServiceWorker,
enableDataCollectionPermissions: this.config.enableDataCollectionPermissions
}
});
await manifestParser.validateIcons();
if (manifestParser.isStaticTheme) {
await manifestParser.validateStaticThemeImages();
}
this.addonMetadata = manifestParser.getMetadata();
} else {
_log.warn(`No ${constants.MANIFEST_JSON} was found in the package metadata`);
this.collector.addError(messages.TYPE_NO_MANIFEST_JSON);
this.addonMetadata = {};
}
this.addonMetadata.totalScannedFileSize = 0;
return this.addonMetadata;
}
async checkFileExists(filepath, _lstat = _utils.lstat) {
const invalidMessage = new Error(`Path "${filepath}" is not a file or directory or does not exist.`);
try {
const stats = await _lstat(filepath);
if (stats.isFile() === true || stats.isDirectory() === true) {
return stats;
}
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
throw invalidMessage;
}
scanFiles(files) {
const promises = [];
files.forEach(filename => {
promises.push(this.scanFile(filename));
});
return Promise.all(promises);
}
getScanner(filename) {
const filenameWithoutPath = _path.default.basename(filename);
if (filename.match(constants.HIDDEN_FILE_REGEX) || filename.match(constants.FLAGGED_FILE_REGEX) || constants.FLAGGED_FILE_EXTENSIONS.includes(_path.default.extname(filename)) || filename.match(constants.ALREADY_SIGNED_REGEX) || constants.RESERVED_FILENAMES.includes(filenameWithoutPath)) {
return _filename2.default;
}
switch (_path.default.extname(filename)) {
case '.html':
case '.htm':
return _html.default;
case '.js':
case '.jsm':
case '.mjs':
return _javascript.default;
case '.json':
return _json.default;
case '.properties':
case '.ftl':
case '.dtd':
return _langpack.default;
default:
return _binary.default;
}
}
async scanFile(filename) {
let scanResult = {
linterMessages: [],
scannedFiles: []
};
const ScannerClass = this.getScanner(filename);
const fileData = await this.io.getFile(filename, ScannerClass.fileResultType);
// First: check that this file is under our 2MB parsing limit. Otherwise
// it will be very slow and may crash the lint with an out-of-memory
// error.
const fileSize = typeof this.io.files[filename].size !== 'undefined' ? this.io.files[filename].size : this.io.files[filename].uncompressedSize;
const maxSize = 1024 * 1024 * constants.MAX_FILE_SIZE_TO_PARSE_MB;
if (ScannerClass !== _binary.default && ScannerClass !== _filename2.default && fileSize >= maxSize) {
const filesizeError = {
...messages.FILE_TOO_LARGE,
file: filename,
type: constants.VALIDATION_ERROR
};
scanResult = {
linterMessages: [filesizeError],
scannedFiles: [filename]
};
} else {
if (ScannerClass !== _binary.default && ScannerClass !== _filename2.default) {
// Check for coin miners
this._markCoinMinerUsage(filename, fileData);
if (this.addonMetadata) {
this.addonMetadata.totalScannedFileSize += fileSize;
}
}
const scanner = new ScannerClass(fileData, filename, {
addonMetadata: this.addonMetadata,
// This is for the JSONScanner, which is a bit of an anomaly and
// accesses the collector directly.
// TODO: Bring this in line with other scanners, see:
// https://github.com/mozilla/addons-linter/issues/895
collector: this.collector,
// list of disabled rules for js scanner
disabledRules: this.config.disableLinterRules,
existingFiles: this.io.files,
enterprise: this.config.enterprise,
privileged: this.config.privileged
});
scanResult = await scanner.scan();
}
// messages should be a list of raw message data objects.
const {
linterMessages,
scannedFiles
} = scanResult;
linterMessages.forEach(message => {
if (typeof message.type === 'undefined') {
throw new Error('message.type must be defined');
}
this.collector._addMessage(message.type, message);
});
scannedFiles.forEach(_filename => {
this.collector.recordScannedFile(_filename, ScannerClass.scannerName);
});
}
async extractMetadata({
_Crx = _io.Crx,
_Directory = _io.Directory,
_Xpi = _io.Xpi,
_console = console
} = {}) {
await (0, _utils2.checkMinNodeVersion)();
const stats = await this.checkFileExists(this.packagePath);
// Simple logging adapter for addons-scanner-utils IO.
const stderr = {
debug: message => _logger.default.debug(message),
error: message => _logger.default.error(message),
info: message => _logger.default.info(message)
};
if (stats.isFile()) {
if (this.packagePath.endsWith('.crx')) {
_logger.default.info('Package is a file ending in .crx; parsing as a CRX');
this.io = new _Crx({
filePath: this.packagePath,
stderr
});
} else {
_logger.default.info('Package is a file. Attempting to parse as an .xpi/.zip');
// We should set `autoClose` to `false` when we want to disable this
// feature. By default, the auto-close feature is enabled.
const autoClose = this.config.disableXpiAutoclose !== true;
if (!autoClose) {
_logger.default.info('Disabling the auto-close feature');
}
this.io = new _Xpi({
autoClose,
filePath: this.packagePath,
stderr
});
}
} else {
// If not a file then it's a directory.
_logger.default.info('Package path is a directory. Parsing as a directory');
this.io = new _Directory({
filePath: this.packagePath,
stderr
});
}
this.io.setScanFileCallback(this.shouldScanFile);
let addonMetadata = await this.getAddonMetadata();
addonMetadata = await this.markSpecialFiles(addonMetadata);
_logger.default.info('Metadata option is set to %s', this.config.metadata);
if (this.config.metadata === true) {
const metadataObject = {
// Reflects if errors were encountered in extraction
// of metadata.
hasErrors: this.output.errors.length !== 0,
metadata: addonMetadata
};
// If errors exist the data is available via the
// errors list.
if (metadataObject.hasErrors) {
metadataObject.errors = this.output.errors;
}
_console.log(this.toJSON({
input: metadataObject
}));
}
return addonMetadata;
}
shouldScanFile(fileOrDirName, isDir) {
if (this.config.shouldScanFile) {
return this.config.shouldScanFile(fileOrDirName, isDir);
}
// Defaults to true.
return true;
}
async scan(deps = {}) {
try {
await this.extractMetadata(deps);
const files = await this.io.getFiles();
// Known libraries do not need to be scanned
const filesWithoutJSLibraries = Object.keys(files).filter(file => {
return !Object.prototype.hasOwnProperty.call(this.addonMetadata.jsLibs, file);
});
await this.scanFiles(filesWithoutJSLibraries);
this.closeIO();
this.print(deps._console);
// This is skipped in code coverage because the
// test runs against un-instrumented code.
/* istanbul ignore if */
if (this.config.runAsBinary === true) {
let exitCode = this.output.errors.length > 0 ? 1 : 0;
if (exitCode === 0 && this.config.warningsAsErrors === true) {
exitCode = this.output.warnings.length > 0 ? 1 : 0;
}
process.exit(exitCode);
}
} catch (err) {
if (this.handleError(err, deps._console)) {
return;
}
throw err;
}
}
async run(deps = {}) {
// Validate the config options from a linter perspective (in addition to the
// yargs validation that already happened when the options are being parsed)
// and throws if there are invalid options.
this.validateConfig();
if (this.config.metadata === true) {
try {
await this.extractMetadata(deps);
this.closeIO();
// This is skipped in the code coverage because the
// test runs against un-instrumented code.
/* istanbul ignore if */
if (this.config.runAsBinary === true) {
process.exit(this.output.errors.length > 0 ? 1 : 0);
}
return this.output;
} catch (err) {
_logger.default.debug(err);
this.handleError(err, deps._console);
throw err;
}
}
await this.scan(deps);
return this.output;
}
async markSpecialFiles(addonMetadata) {
let _addonMetadata = await this._markEmptyFiles(addonMetadata);
_addonMetadata = await this._markJSLibs(_addonMetadata);
_addonMetadata = this._markBannedLibs(_addonMetadata);
return this._markUnknownOrMinifiedCode(_addonMetadata);
}
_markBannedLibs(addonMetadata, _unadvisedLibraries = _libraries.UNADVISED_LIBRARIES) {
Object.keys(addonMetadata.jsLibs).forEach(pathToFile => {
if (_libraries.BANNED_LIBRARIES.includes(addonMetadata.jsLibs[pathToFile])) {
this.collector.addError({
...messages.BANNED_LIBRARY,
file: pathToFile
});
}
if (_unadvisedLibraries.includes(addonMetadata.jsLibs[pathToFile])) {
this.collector.addWarning({
...messages.UNADVISED_LIBRARY,
file: pathToFile
});
}
});
return addonMetadata;
}
async _markEmptyFiles(addonMetadata) {
const emptyFiles = [];
const files = await this.io.getFiles();
Object.keys(files).forEach(filename => {
if (typeof files[filename].size === 'undefined' && typeof files[filename].uncompressedSize === 'undefined') {
throw new Error(`No size available for ${filename}`);
}
if (files[filename].size === 0 || files[filename].uncompressedSize === 0) {
emptyFiles.push(filename);
}
});
// eslint-disable-next-line no-param-reassign
addonMetadata.emptyFiles = emptyFiles;
return addonMetadata;
}
async _markJSLibs(addonMetadata) {
const dispensary = new _dispensary.default();
const jsLibs = {};
const files = await this.io.getFilesByExt('.js');
await Promise.all(files.map(async filename => {
const file = await this.io.getFile(filename);
const hashResult = dispensary.match(file);
if (hashResult !== false) {
_logger.default.debug(`${hashResult} detected in ${filename}`);
jsLibs[filename] = hashResult;
this.collector.addNotice({
...messages.KNOWN_LIBRARY,
file: filename
});
}
}));
// eslint-disable-next-line no-param-reassign
addonMetadata.jsLibs = jsLibs;
return addonMetadata;
}
async _markUnknownOrMinifiedCode(addonMetadata) {
const unknownMinifiedFiles = [];
const files = await this.io.getFilesByExt('.js');
await Promise.all(files.map(async filename => {
if (filename in addonMetadata.jsLibs) {
return;
}
const fileData = await this.io.getFile(filename);
if ((0, _utils2.couldBeMinifiedCode)(fileData)) {
_logger.default.debug(`Minified code detected in ${filename}`);
unknownMinifiedFiles.push(filename);
}
}));
// eslint-disable-next-line no-param-reassign
addonMetadata.unknownMinifiedFiles = unknownMinifiedFiles;
return addonMetadata;
}
_markCoinMinerUsage(filename, fileData) {
if (fileData && fileData.trim()) {
_miner_blocklist.MINER_BLOCKLIST.filenames.forEach(nameRegex => {
const filenameMatch = filename.match(nameRegex);
if (filenameMatch) {
this.collector.addWarning({
...messages.COINMINER_USAGE_DETECTED,
file: filename
});
}
const fileDataMatch = fileData.match(nameRegex);
if (fileDataMatch) {
const {
matchedLine,
matchedColumn
} = (0, _utils2.getLineAndColumnFromMatch)(fileDataMatch);
this.collector.addWarning({
...messages.COINMINER_USAGE_DETECTED,
file: filename,
column: matchedColumn,
line: matchedLine
});
}
});
_miner_blocklist.MINER_BLOCKLIST.code.forEach(codeRegex => {
const match = fileData.match(codeRegex);
if (match) {
const {
matchedLine,
matchedColumn
} = (0, _utils2.getLineAndColumnFromMatch)(match);
this.collector.addWarning({
...messages.COINMINER_USAGE_DETECTED,
file: filename,
line: matchedLine,
column: matchedColumn,
// use instancePath for our actual match to avoid any obvious
// duplicates
instancePath: match[0]
});
}
});
}
}
}
exports["default"] = Linter;
/***/ }),
/* 10 */
/***/ ((module) => {
module.exports = require("path");
/***/ }),
/* 11 */
/***/ ((module) => {
module.exports = require("columnify");
/***/ }),
/* 12 */
/***/ ((module) => {
module.exports = require("chalk");
/***/ }),
/* 13 */
/***/ ((module) => {
module.exports = require("addons-scanner-utils/io/utils");
/***/ }),
/* 14 */
/***/ ((module) => {
module.exports = require("addons-scanner-utils/errors");
/***/ }),
/* 15 */
/***/ ((module) => {
module.exports = require("addons-scanner-utils/io");
/***/ }),
/* 16 */
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({
value: true
}));
exports.UNADVISED_LIBRARIES = exports.BANNED_LIBRARIES = void 0;
const BANNED_LIBRARIES = exports.BANNED_LIBRARIES = ['angularjs.1.0.2.angular.js', 'angularjs.1.0.2.angular.min.js', 'angularjs.1.0.3.angular.js', 'angularjs.1.0.3.angular.min.js', 'angularjs.1.0.4.angular.js', 'angularjs.1.0.4.angular.min.js', 'angularjs.1.0.5.angular.js', 'angularjs.1.0.5.angular.min.js', 'angularjs.1.0.6.angular.js', 'angularjs.1.0.6.angular.min.js', 'angularjs.1.0.7.angular.js', 'angularjs.1.0.7.angular.min.js', 'angularjs.1.0.8.angular.js', 'angularjs.1.0.8.angular.min.js', 'angularjs.1.1.0.angular.js', 'angularjs.1.1.0.angular.min.js', 'angularjs.1.1.1.angular.js', 'angularjs.1.1.1.angular.min.js', 'angularjs.1.1.2.angular.js', 'angularjs.1.1.2.angular.min.js', 'angularjs.1.1.3.angular.js', 'angularjs.1.1.3.angular.min.js', 'angularjs.1.1.4.angular.js', 'angularjs.1.1.4.angular.min.js', 'angularjs.1.1.5.angular.js', 'angularjs.1.1.5.angular.min.js', 'angularjs.1.2.0.angular.js', 'angularjs.1.2.0.angular.min.js', 'angularjs.1.2.1.angular.js', 'angularjs.1.2.1.angular.min.js', 'angularjs.1.2.2.angular.js', 'angularjs.1.2.2.angular.min.js', 'angularjs.1.2.3.angular.js', 'angularjs.1.2.3.angular.min.js', 'angularjs.1.2.4.angular.js', 'angularjs.1.2.4.angular.min.js', 'angularjs.1.2.5.angular.js', 'angularjs.1.2.5.angular.min.js', 'angularjs.1.2.6.angular.js', 'angularjs.1.2.6.angular.min.js', 'angularjs.1.2.7.angular.js', 'angularjs.1.2.7.angular.min.js', 'angularjs.1.2.8.angular.js', 'angularjs.1.2.8.angular.min.js', 'angularjs.1.2.9.angular.js', 'angularjs.1.2.9.angular.min.js', 'angularjs.1.2.10.angular.js', 'angularjs.1.2.10.angular.min.js', 'angularjs.1.2.11.angular.js', 'angularjs.1.2.11.angular.min.js', 'angularjs.1.2.12.angular.js', 'angularjs.1.2.12.angular.min.js', 'angularjs.1.2.13.angular.js', 'angularjs.1.2.13.angular.min.js', 'angularjs.1.2.14.angular.js', 'angularjs.1.2.14.angular.min.js', 'angularjs.1.2.15.angular.js', 'angularjs.1.2.15.angular.min.js', 'angularjs.1.2.16.angular.js', 'angularjs.1.2.16.angular.min.js', 'angularjs.1.2.17.angular.js', 'angularjs.1.2.17.angular.min.js', 'angularjs.1.2.18.angular.js', 'angularjs.1.2.18.angular.min.js', 'angularjs.1.2.19.angular.js', 'angularjs.1.2.19.angular.min.js', 'angularjs.1.2.20.angular.js', 'angularjs.1.2.20.angular.min.js', 'angularjs.1.2.21.angular.js', 'angularjs.1.2.21.angular.min.js', 'angularjs.1.2.22.angular.js', 'angularjs.1.2.22.angular.min.js', 'angularjs.1.2.23.angular.js', 'angularjs.1.2.23.angular.min.js', 'angularjs.1.2.24.angular.js', 'angularjs.1.2.24.angular.min.js', 'angularjs.1.2.25.angular.js', 'angularjs.1.2.25.angular.min.js', 'angularjs.1.2.26.angular.js', 'angularjs.1.2.26.angular.min.js', 'angularjs.1.2.27.angular.js', 'angularjs.1.2.27.angular.min.js', 'angularjs.1.2.28.angular.js', 'angularjs.1.2.28.angular.min.js', 'angularjs.1.2.29.angular.js', 'angularjs.1.2.29.angular.min.js', 'angularjs.1.2.30.angular.js', 'angularjs.1.2.30.angular.min.js', 'angularjs.1.3.0.angular.js', 'angularjs.1.3.0.angular.min.js', 'angularjs.1.3.1.angular.js', 'angularjs.1.3.1.angular.min.js', 'angularjs.1.3.2.angular.js', 'angularjs.1.3.2.angular.min.js', 'angularjs.1.3.3.angular.js', 'angularjs.1.3.3.angular.min.js', 'angularjs.1.3.4.angular.js', 'angularjs.1.3.4.angular.min.js', 'angularjs.1.3.5.angular.js', 'angularjs.1.3.5.angular.min.js', 'angularjs.1.3.6.angular.js', 'angularjs.1.3.6.angular.min.js', 'angularjs.1.3.7.angular.js', 'angularjs.1.3.7.angular.min.js', 'angularjs.1.3.8.angular.js', 'angularjs.1.3.8.angular.min.js', 'angularjs.1.3.9.angular.js', 'angularjs.1.3.9.angular.min.js', 'angularjs.1.3.10.angular.js', 'angularjs.1.3.10.angular.min.js', 'angularjs.1.3.11.angular.js', 'angularjs.1.3.11.angular.min.js', 'angularjs.1.3.12.angular.js', 'angularjs.1.3.12.angular.min.js', 'angularjs.1.3.13.angular.js', 'angularjs