UNPKG

addons-linter

Version:
1,187 lines (1,109 loc) 912 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__(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