addons-linter
Version:
Mozilla Add-ons Linter
1,311 lines (1,065 loc) • 855 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__(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