UNPKG

addons-linter

Version:
1,311 lines (1,065 loc) 855 kB
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__(8)); var _logger = _interopRequireDefault(__webpack_require__(4)); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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 = { Linter: _linter.default, createInstance, isRunFromCLI }; exports["default"] = _default; /***/ }), /* 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__(7); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function createLogger(_process = process) { const level = _process.env.LOG_LEVEL || 'fatal'; return (0, _pino.default)({ name: 'AddonLinterJS', level }, process.stdout); } var _default = createLogger(); exports["default"] = _default; /***/ }), /* 5 */ /***/ ((module) => { module.exports = require("pino"); /***/ }), /* 6 */ /***/ ((__unused_webpack_module, exports) => { Object.defineProperty(exports, "__esModule", ({ value: true })); exports["default"] = void 0; exports.getDefaultConfigValue = getDefaultConfigValue; const options = { 'log-level': { describe: 'The log-level to generate', type: 'string', default: 'fatal', choices: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'] }, 'warnings-as-errors': { describe: 'Treat warning as errors', type: 'boolean', default: false }, output: { alias: 'o', describe: 'The type of output to generate', type: 'string', default: 'text', choices: ['json', 'text'] }, metadata: { describe: 'Output only metadata as JSON', type: 'boolean', default: false }, pretty: { describe: 'Prettify JSON output', type: 'boolean', default: false }, stack: { describe: 'Show stacktraces when errors are thrown', type: 'boolean', default: false }, boring: { describe: 'Disables colorful shell output', type: 'boolean', default: false }, 'self-hosted': { describe: 'Disables messages related to hosting on addons.mozilla.org.', type: 'boolean', default: false }, 'min-manifest-version': { describe: 'Set a custom minimum allowed value for the manifest_version property', type: 'number', default: 2, requiresArg: true }, 'max-manifest-version': { describe: 'Set a custom maximum allowed value for the manifest_version property', type: 'number', default: 2, requiresArg: true }, 'scan-file': { alias: ['f'], describe: 'Scan a selected file', type: 'string', 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: false } }; var _default = options; exports["default"] = _default; function getDefaultConfigValue(name) { if (options[name] && 'default' in options[name]) { return options[name].default; } return undefined; } /***/ }), /* 7 */ /***/ ((module) => { module.exports = JSON.parse('{"name":"addons-linter","version":"4.6.0","description":"Mozilla Add-ons Linter","main":"dist/addons-linter.js","bin":{"addons-linter":"bin/addons-linter"},"engines":{"node":">=12.21.0"},"browserslist":["node >=12.21.0"],"scripts":{"build":"webpack --bail --stats-error-details true --color --config webpack.config.js","eslint":"eslint bin/* scripts/* .","extract-locales":"webpack --bail --stats-error-details true --color --config webpack.l10n.config.babel.js","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","gen-contributing-toc":"doctoc CONTRIBUTING.md","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":{"@mdn/browser-compat-data":"4.1.2","addons-moz-compare":"1.2.0","addons-scanner-utils":"6.2.0","ajv":"6.12.6","ajv-merge-patch":"4.1.0","chalk":"4.1.2","cheerio":"1.0.0-rc.10","columnify":"1.5.4","common-tags":"1.8.2","deepmerge":"4.2.2","eslint":"8.6.0","eslint-plugin-no-unsanitized":"4.0.1","eslint-visitor-keys":"3.1.0","espree":"9.3.0","esprima":"4.0.1","fluent-syntax":"0.13.0","glob":"7.2.0","image-size":"1.0.1","is-mergeable-object":"1.1.1","jed":"1.1.1","os-locale":"5.0.0","pino":"7.6.3","postcss":"8.4.5","relaxed-json":"1.0.3","semver":"7.3.5","sha.js":"2.4.11","source-map-support":"0.5.21","tosource":"1.0.0","upath":"2.0.1","yargs":"17.3.1","yauzl":"2.10.0"},"devDependencies":{"@babel/cli":"7.16.8","@babel/core":"7.16.7","@babel/eslint-parser":"7.16.5","@babel/plugin-proposal-class-properties":"7.16.7","@babel/plugin-proposal-decorators":"7.16.7","@babel/plugin-proposal-export-namespace-from":"7.16.7","@babel/plugin-proposal-function-sent":"7.16.7","@babel/plugin-proposal-numeric-separator":"7.16.7","@babel/plugin-proposal-throw-expressions":"7.16.7","@babel/preset-env":"7.16.8","@babel/register":"7.16.8","async":"3.2.3","babel-core":"7.0.0-bridge.0","babel-gettext-extractor":"4.1.3","babel-jest":"27.4.6","babel-loader":"8.2.3","comment-json":"4.1.1","doctoc":"2.1.0","eslint-config-amo":"5.3.0","github-markdown-css":"5.1.0","gunzip-maybe":"1.4.2","hashish":"0.0.4","jest":"27.4.7","jest-raw-loader":"1.0.1","lodash.clonedeep":"4.5.0","lodash.ismatchwith":"4.4.0","markdown-it":"12.3.2","markdown-it-anchor":"8.4.1","markdown-it-emoji":"2.0.0","natural-compare-lite":"1.4.0","node-fetch":"2.6.6","po2json":"1.0.0-beta-3","prettier":"2.5.1","pretty-quick":"3.1.3","raw-loader":"4.0.2","shelljs":"0.8.5","sinon":"12.0.1","tar":"6.1.11","tar-fs":"2.1.1","tmp-promise":"3.0.3","webpack":"5.65.0","webpack-cli":"4.9.1","webpack-node-externals":"3.0.0","yazl":"2.5.1"}}'); /***/ }), /* 8 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { Object.defineProperty(exports, "__esModule", ({ value: true })); exports["default"] = void 0; var _path = _interopRequireDefault(__webpack_require__(9)); var _columnify = _interopRequireDefault(__webpack_require__(10)); var _chalk = _interopRequireDefault(__webpack_require__(11)); var _commonTags = __webpack_require__(3); var _utils = __webpack_require__(12); var _errors = __webpack_require__(13); var _io = __webpack_require__(14); var _cli = __webpack_require__(1); var constants = _interopRequireWildcard(__webpack_require__(15)); 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__(32)); var _manifestjson = _interopRequireDefault(__webpack_require__(34)); var _binary = _interopRequireDefault(__webpack_require__(112)); var _css = _interopRequireDefault(__webpack_require__(114)); var _filename2 = _interopRequireDefault(__webpack_require__(118)); var _html = _interopRequireDefault(__webpack_require__(119)); var _javascript = _interopRequireDefault(__webpack_require__(124)); var _json = _interopRequireDefault(__webpack_require__(143)); var _langpack = _interopRequireDefault(__webpack_require__(145)); var _miner_blocklist = __webpack_require__(150); var _dispensary = _interopRequireDefault(__webpack_require__(151)); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } class Linter { constructor(config) { this.config = config; [this.packagePath] = config._; this.io = null; this.chalk = new _chalk.default.Instance({ enabled: !this.config.boring }); this.collector = new _collector.default(config); this.addonMetadata = null; this.shouldScanFile = this.shouldScanFile.bind(this); } set config(cfg) { this._config = cfg; // normalize the scanFile option: // convert into an array if needed and filter out any undefined // or empty strings. if (this._config.scanFile) { let scanFile = Array.isArray(this._config.scanFile) ? this._config.scanFile : [this._config.scanFile]; scanFile = scanFile.filter(el => el && el.length > 0); this._config.scanFile = scanFile; } } get config() { return this._config; } validateConfig() { const { minManifestVersion, maxManifestVersion } = this.config; if (maxManifestVersion < minManifestVersion) { throw new _utils2.AddonsLinterUserError(_utils2.i18n._((0, _commonTags.oneLine)` Invalid manifest version range requested: --min-manifest-version (currently set to ${minManifestVersion}) should not be greater than --max-manifest-version (currently set to ${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 })); 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 })); } }); if (this.output.scanFile) { out.push(`Selected files: ${this.output.scanFile.join(', ')}`); out.push(''); } return out.join('\n'); } get output() { const output = { count: this.collector.length, summary: {}, metadata: this.addonMetadata }; if (this.config.scanFile) { output.scanFile = this.config.scanFile; } 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, selfHosted: this.config.selfHosted, schemaValidatorOptions: { minManifestVersion: this.config.minManifestVersion, maxManifestVersion: this.config.maxManifestVersion } }); 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 '.css': return _css.default; 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 && 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 }); 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); } if (this.config.scanFile) { const manifestFileNames = ['manifest.json', 'package.json']; // Always scan sub directories and the manifest files, // or the linter will not be able to detect the addon type. if (isDir || manifestFileNames.includes(fileOrDirName)) { return true; } return this.config.scanFile.some(v => v === fileOrDirName); } // Defaults to true. return true; } async scan(deps = {}) { try { await this.extractMetadata(deps); const files = await this.io.getFiles(); if (this.config.scanFile && !this.config.scanFile.some(f => Object.keys(files).includes(f))) { const _files = this.config.scanFile.join(', '); throw new Error(`Selected file(s) not found: ${_files}`); } // 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 dataPath for our actual match to avoid any obvious // duplicates dataPath: match[0] }); } }); } } } exports["default"] = Linter; /***/ }), /* 9 */ /***/ ((module) => { module.exports = require("path"); /***/ }), /* 10 */ /***/ ((module) => { module.exports = require("columnify"); /***/ }), /* 11 */ /***/ ((module) => { module.exports = require("chalk"); /***/ }), /* 12 */ /***/ ((module) => { module.exports = require("addons-scanner-utils/dist/io/utils"); /***/ }), /* 13 */ /***/ ((module) => { module.exports = require("addons-scanner-utils/dist/errors"); /***/ }), /* 14 */ /***/ ((module) => { module.exports = require("addons-scanner-utils/dist/io"); /***/ }), /* 15 */ /***/ ((__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.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.LOCAL_PROTOCOLS = 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.CSP_KEYWORD_RE = exports.ALREADY_SIGNED_REGEX = void 0; const ESLINT_ERROR = 2; exports.ESLINT_ERROR = ESLINT_ERROR; const ESLINT_WARNING = 1; exports.ESLINT_WARNING = ESLINT_WARNING; 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: [] } }; // 3rd party / eslint-internal rules const 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/property': [ESLINT_WARNING, NO_UNSANITIZED_OPTIONS] }; exports.EXTERNAL_RULE_MAPPING = EXTERNAL_RULE_MAPPING; const ESLINT_RULE_MAPPING = { 'deprecated-entities': ESLINT_WARNING, 'event-listener-fourth': ESLINT_WARNING, 'global-require-arg': ESLINT_WARNING, 'opendialog-nonlit-uri': ESLINT_WARNING, 'opendialog-remote-uri': 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 }; exports.ESLINT_RULE_MAPPING = ESLINT_RULE_MAPPING; const VALIDATION_ERROR = 'error'; exports.VALIDATION_ERROR = VALIDATION_ERROR; const VALIDATION_NOTICE = 'notice'; exports.VALIDATION_NOTICE = VALIDATION_NOTICE; const VALIDATION_WARNING = 'warning'; exports.VALIDATION_WARNING = VALIDATION_WARNING; const ESLINT_TYPES = { 0: VALIDATION_NOTICE, 1: VALIDATION_WARNING, 2: VALIDATION_ERROR }; exports.ESLINT_TYPES = ESLINT_TYPES; const MESSAGE_TYPES = [VALIDATION_ERROR, VALIDATION_NOTICE, VALIDATION_WARNING]; // Package type constants. exports.MESSAGE_TYPES = MESSAGE_TYPES; const PACKAGE_ANY = 0; exports.PACKAGE_ANY = PACKAGE_ANY; const PACKAGE_EXTENSION = 1; exports.PACKAGE_EXTENSION = PACKAGE_EXTENSION; const PACKAGE_THEME = 2; exports.PACKAGE_THEME = PACKAGE_THEME; const PACKAGE_DICTIONARY = 3; exports.PACKAGE_DICTIONARY = PACKAGE_DICTIONARY; const PACKAGE_LANGPACK = 4; exports.PACKAGE_LANGPACK = PACKAGE_LANGPACK; const PACKAGE_SEARCHPROV = 5; exports.PACKAGE_SEARCHPROV = PACKAGE_SEARCHPROV; const PACKAGE_MULTI = 1; // A multi extension is an extension exports.PACKAGE_MULTI = PACKAGE_MULTI; const PACKAGE_SUBPACKAGE = 7; exports.PACKAGE_SUBPACKAGE = PACKAGE_SUBPACKAGE; const PACKAGE_TYPES = { PACKAGE_ANY, PACKAGE_EXTENSION, PACKAGE_THEME, PACKAGE_DICTIONARY, PACKAGE_LANGPACK, PACKAGE_SEARCHPROV, PACKAGE_MULTI, PACKAGE_SUBPACKAGE }; exports.PACKAGE_TYPES = PACKAGE_TYPES; const LOCAL_PROTOCOLS = ['chrome:', 'resource:']; exports.LOCAL_PROTOCOLS = LOCAL_PROTOCOLS; const 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). exports.MANIFEST_JSON = MANIFEST_JSON; const MANIFEST_VERSION_DEFAULT = 2; // Default min/max_manifest_version values used for schema definitions that do not // have an explicit one on their own. exports.MANIFEST_VERSION_DEFAULT = MANIFEST_VERSION_DEFAULT; const MANIFEST_VERSION_MIN = 2; exports.MANIFEST_VERSION_MIN = MANIFEST_VERSION_MIN; const 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 4MB 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 // // We should be careful about increasing this any further. exports.MANIFEST_VERSION_MAX = MANIFEST_VERSION_MAX; const MAX_FILE_SIZE_TO_PARSE_MB = 4; exports.MAX_FILE_SIZE_TO_PARSE_MB = MAX_FILE_SIZE_TO_PARSE_MB; const HIDDEN_FILE_REGEX = /^__MACOSX\//; exports.HIDDEN_FILE_REGEX = HIDDEN_FILE_REGEX; const FLAGGED_FILE_REGEX = /thumbs\.db$|\.DS_Store$|\.orig$|\.old$|~$/i; exports.FLAGGED_FILE_REGEX = FLAGGED_FILE_REGEX; const ALREADY_SIGNED_REGEX = /^META-INF\/manifest\.mf/; exports.ALREADY_SIGNED_REGEX = ALREADY_SIGNED_REGEX; const PERMS_DATAPATH_REGEX = /^\/(permissions|optional_permissions|host_permissions)\/([\d+])/; exports.PERMS_DATAPATH_REGEX = PERMS_DATAPATH_REGEX; const INSTALL_ORIGINS_DATAPATH_REGEX = /^\/(install_origins)\/([\d+])/; exports.INSTALL_ORIGINS_DATAPATH_REGEX = INSTALL_ORIGINS_DATAPATH_REGEX; const RESERVED_FILENAMES = ['mozilla-recommendation.json']; exports.RESERVED_FILENAMES = RESERVED_FILENAMES; const FLAGGED_FILE_EXTENSIONS = ['.class', '.dll', '.dylib', '.exe', '.jar', '.sh', '.so', '.swf']; exports.FLAGGED_FILE_EXTENSIONS = FLAGGED_FILE_EXTENSIONS; const IMAGE_FILE_EXTENSIONS = ['jpg', 'jpeg', 'webp', 'gif', 'png', 'svg']; exports.IMAGE_FILE_EXTENSIONS = IMAGE_FILE_EXTENSIONS; const 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. exports.FILE_EXTENSIONS_TO_MIME = FILE_EXTENSIONS_TO_MIME; const 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. exports.STATIC_THEME_IMAGE_MIMES = STATIC_THEME_IMAGE_MIMES; const 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. exports.DEPRECATED_MANIFEST_PROPERTIES = DEPRECATED_MANIFEST_PROPERTIES; const 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. exports.DEPRECATED_JAVASCRIPT_APIS = DEPRECATED_JAVASCRIPT_APIS; const 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 exports.FLAGGED_FILE_MAGIC_NUMBERS = FLAGGED_FILE_MAGIC_NUMBERS; const 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. exports.TEMPORARY_APIS = TEMPORARY_APIS; const CSP_KEYWORD_RE = new RegExp(['(self|none|unsafe-inline|strict-dynamic|unsafe-hashed-attributes)', // Only match these keywords, anything else is forbidden '(?!.)', '|(sha(256|384|512)-|nonce-)'].join('')); exports.CSP_KEYWORD_RE = CSP_KEYWORD_RE; const MESSAGES_JSON = 'messages.json'; exports.MESSAGES_JSON = MESSAGES_JSON; const 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 exports.LOCALES_DIRECTORY = LOCALES_DIRECTORY; const MESSAGE_PLACEHOLDER_REGEXP = '\\$([a-zA-Z0-9_@]+)\\$'; // yauzl should trow error with this message in case of corrupt zip file exports.MESSAGE_PLACEHOLDER_REGEXP = MESSAGE_PLACEHOLDER_REGEXP; const 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 exports.ZIP_LIB_CORRUPT_FILE_ERROR = ZIP_LIB_CORRUPT_FILE_ERROR; const 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). exports.RESTRICTED_HOMEPAGE_URLS = RESTRICTED_HOMEPAGE_URLS; const RESTRICTED_PERMISSIONS = new Map([// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1733159 ['proxy', '91.1.0']]); exports.RESTRICTED_PERMISSIONS = RESTRICTED_PERMISSIONS; /***/ }), /* 16 */ /***/ ((__unused_webpack_module, exports) => { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.UNADVISED_LIBRARIES = exports.BANNED_LIBRARIES = void 0; const 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