eslint-plugin-chrome-extensions 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +646 -33
- package/docs/LICENSE.md +1 -1
- package/package.json +47 -36
- package/src/data/apis.js +28 -29
- package/src/data/i18n.js +71 -0
- package/src/data/license.js +27 -0
- package/src/data/manifest.js +14 -13
- package/src/data/text-parser.js +11 -0
- package/src/index.js +53 -1
- package/src/rules/manifest-key-order.js +1 -3
- package/src/rules/manifest-spacing.js +12 -4
- package/src/rules/no-content-style-leak.js +5 -2
- package/src/rules/no-invalid-permissions.js +2 -2
- package/src/rules/no-missing-default-locale.js +91 -0
- package/src/rules/no-missing-i18n-keys.js +72 -0
- package/src/rules/no-missing-permissions.js +16 -3
- package/src/rules/no-missing-web-accessible-resources.js +4 -4
- package/src/rules/no-remote-code.js +5 -6
- package/src/rules/no-unrestricted-host-permissions.js +5 -4
- package/src/rules/no-unused-i18n-keys.js +54 -0
- package/src/rules/no-unused-permissions.js +3 -2
- package/src/rules/require-license-file.js +27 -0
- package/src/rules/require-short-name-when-useful.js +71 -0
- package/src/rules/valid-filenames.js +151 -0
- package/src/rules/valid-license-file.js +59 -0
- package/src/rules/valid-mv3-schema.js +2 -1
- package/src/rules/valid-service-worker.js +3 -2
- package/src/rules/valid-url-fields.js +136 -0
- package/src/rules/valid-version-field.js +4 -2
- package/src/utils.js +12 -13
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getFilename } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = {
|
|
4
|
+
absoluteURL: /^[a-z][a-z0-9+.-]*:\/\/[^\s/]+/i, manifestFilename: /manifest\.json$/,
|
|
5
|
+
match: /^(\*|https?|file|ftp):\/\/([^/]*)(\/.*)$/, template: /\{[^}]+\}/g
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const matchPaths = new Set([
|
|
9
|
+
'content_scripts.matches', 'externally_connectable.matches',
|
|
10
|
+
'host_permissions', 'optional_host_permissions', 'web_accessible_resources.matches'
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
const urlPaths = new Set([
|
|
14
|
+
'homepage_url', 'update_url', 'oauth2.scopes',
|
|
15
|
+
'chrome_settings_overrides.homepage',
|
|
16
|
+
'chrome_settings_overrides.search_provider.alternate_urls',
|
|
17
|
+
'chrome_settings_overrides.search_provider.favicon_url',
|
|
18
|
+
'chrome_settings_overrides.search_provider.image_url',
|
|
19
|
+
'chrome_settings_overrides.search_provider.instant_url',
|
|
20
|
+
'chrome_settings_overrides.search_provider.search_url',
|
|
21
|
+
'chrome_settings_overrides.search_provider.suggest_url',
|
|
22
|
+
'chrome_settings_overrides.startup_pages'
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
function collectStrings(obj, path = [], out = []) {
|
|
26
|
+
if (obj == null) return out
|
|
27
|
+
if (typeof obj == 'string') out.push({ path: path.join('.'), value: obj })
|
|
28
|
+
else if (Array.isArray(obj))
|
|
29
|
+
for (const el of obj) collectStrings(el, path, out)
|
|
30
|
+
else if (typeof obj == 'object')
|
|
31
|
+
for (const key of Object.keys(obj)) // eslint-disable-next-line security/detect-object-injection
|
|
32
|
+
collectStrings(obj[key], [...path, key], out)
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isURL(value) { return re.absoluteURL.test(value.replace(re.template, 'x')) }
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
meta: {
|
|
40
|
+
type: 'problem',
|
|
41
|
+
docs: {
|
|
42
|
+
description: 'Validates URLs and match patterns in manifest fields',
|
|
43
|
+
category: 'Possible Errors',
|
|
44
|
+
recommended: true,
|
|
45
|
+
manifest: true
|
|
46
|
+
},
|
|
47
|
+
schema: [{
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
checkURLfields: {
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
description: 'Validate URL fields (`homepage_url`, `update_url`, etc.)'
|
|
53
|
+
},
|
|
54
|
+
checkMatchPatterns: {
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
description: 'Validate match patterns (`host_permissions`, `content_scripts`, etc.)'
|
|
57
|
+
},
|
|
58
|
+
allowedSchemes: {
|
|
59
|
+
type: 'array', items: { type: 'string' },
|
|
60
|
+
description: 'Permitted URL schemes for match patterns (default `["https", "file"]`)'
|
|
61
|
+
},
|
|
62
|
+
allowAllURLs: {
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
description: 'Allow unrestricted patterns (`<all_urls>`, `https://*/*`, `*://*/*`)'
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
additionalProperties: false
|
|
68
|
+
}],
|
|
69
|
+
defaultOptions: [{
|
|
70
|
+
checkURLfields: true, checkMatchPatterns: true,
|
|
71
|
+
allowedSchemes: ['https', 'file'], allowAllURLs: false
|
|
72
|
+
}],
|
|
73
|
+
messages: {
|
|
74
|
+
invalidURL: '`{{field}}` has an invalid URL: "{{value}}".',
|
|
75
|
+
invalidMatchPattern: '`{{field}}` has an invalid match pattern: "{{value}}".',
|
|
76
|
+
invalidScheme: '`{{field}}` uses a disallowed scheme: "{{value}}".',
|
|
77
|
+
invalidFileHost: '`{{field}}` uses a `file://` scheme w/ a non-empty host: "{{value}}".',
|
|
78
|
+
invalidHost: '`{{field}}` has an invalid wildcard host: "{{value}}". Use `*.` prefix instead.',
|
|
79
|
+
invalidWARPath: '`{{field}}` must use an effective path of `/*`: "{{value}}".',
|
|
80
|
+
invalidExternalHost: '`{{field}}` does not allow wildcards in the host: "{{value}}".',
|
|
81
|
+
allURLs: '`{{field}}` uses the unrestricted `<all_urls>` pattern.'
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
create(context) {
|
|
86
|
+
if (!re.manifestFilename.test(getFilename(context))) return {}
|
|
87
|
+
const opts = context.options[0] || {},
|
|
88
|
+
checkURLfields = opts.checkURLfields ?? true,
|
|
89
|
+
checkMatchPatterns = opts.checkMatchPatterns ?? true,
|
|
90
|
+
allowedSchemes = opts.allowedSchemes ?? ['https', 'file'],
|
|
91
|
+
allowAllURLs = opts.allowAllURLs ?? false
|
|
92
|
+
if (!checkURLfields && !checkMatchPatterns) return {}
|
|
93
|
+
return {
|
|
94
|
+
'Document:exit'() {
|
|
95
|
+
let manifest
|
|
96
|
+
try { manifest = JSON.parse(context.sourceCode.text) } catch { return }
|
|
97
|
+
for (const { path, value } of collectStrings(manifest)) {
|
|
98
|
+
if (checkURLfields && urlPaths.has(path)) {
|
|
99
|
+
if (!isURL(value))
|
|
100
|
+
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidURL',
|
|
101
|
+
data: { field: path, value }})
|
|
102
|
+
} else if (checkMatchPatterns && matchPaths.has(path)) {
|
|
103
|
+
if (value == '<all_urls>') {
|
|
104
|
+
if (!allowAllURLs)
|
|
105
|
+
context.report({ loc: { line: 1, column: 1 }, messageId: 'allURLs',
|
|
106
|
+
data: { field: path }})
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const match = value.match(re.match)
|
|
110
|
+
if (!match) {
|
|
111
|
+
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidMatchPattern',
|
|
112
|
+
data: { field: path, value }})
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
const [, scheme, host] = match
|
|
116
|
+
if (scheme != '*' && !allowedSchemes.includes(scheme))
|
|
117
|
+
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidScheme',
|
|
118
|
+
data: { field: path, value }})
|
|
119
|
+
else if (scheme == 'file' && host != '')
|
|
120
|
+
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidFileHost',
|
|
121
|
+
data: { field: path, value }})
|
|
122
|
+
else if (path == 'externally_connectable.matches' && host.includes('*'))
|
|
123
|
+
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidExternalHost',
|
|
124
|
+
data: { field: path, value }})
|
|
125
|
+
else if (host.includes('*') && host != '*' && !host.startsWith('*.'))
|
|
126
|
+
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidHost',
|
|
127
|
+
data: { field: path, value }})
|
|
128
|
+
else if (path == 'web_accessible_resources.matches' && !value.endsWith('/*'))
|
|
129
|
+
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidWARPath',
|
|
130
|
+
data: { field: path, value }})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -30,14 +30,16 @@ export default {
|
|
|
30
30
|
},
|
|
31
31
|
timezone: {
|
|
32
32
|
type: 'string',
|
|
33
|
-
description: 'IANA timezone for determining the current date
|
|
33
|
+
description: 'IANA timezone for determining the current date '
|
|
34
|
+
+ '(when omitted, only year and month are checked)'
|
|
34
35
|
}
|
|
35
36
|
},
|
|
36
37
|
additionalProperties: false
|
|
37
38
|
}],
|
|
38
39
|
defaultOptions: [{}],
|
|
39
40
|
messages: {
|
|
40
|
-
invalidVersion: 'Invalid manifest `version`: must be a valid {{versionType}} version
|
|
41
|
+
invalidVersion: 'Invalid manifest `version`: must be a valid {{versionType}} version '
|
|
42
|
+
+ '(expected format: {{example}}).',
|
|
41
43
|
futureVersion: 'Invalid manifest `version`: date cannot be in the future.'
|
|
42
44
|
}
|
|
43
45
|
},
|
package/src/utils.js
CHANGED
|
@@ -10,17 +10,6 @@ export function bundleRules(rules, predicate, pluginName) {
|
|
|
10
10
|
.map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function initBundleDeprecation(plugin, [ruleId, bundleNames], pluginName) {
|
|
14
|
-
for (const bundleName of bundleNames) {
|
|
15
|
-
/* eslint-disable security/detect-object-injection */
|
|
16
|
-
if (plugin.configs[bundleName])
|
|
17
|
-
plugin.configs[bundleName].rules[`${pluginName}/${ruleId}`] = 'error'
|
|
18
|
-
/* eslint-enable security/detect-object-injection */
|
|
19
|
-
if (process.env.DEBUG_MODE)
|
|
20
|
-
console.warn(`[${pluginName}] '${ruleId}' will be removed from '${bundleName}' in a future major release`)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
13
|
export function findExtensionRoot(context) {
|
|
25
14
|
let dir = path.dirname(getFilename(context))
|
|
26
15
|
while (dir) {
|
|
@@ -55,6 +44,17 @@ export function getMemberPath(node) {
|
|
|
55
44
|
return parts.join('.')
|
|
56
45
|
}
|
|
57
46
|
|
|
47
|
+
export function initBundleDeprecation(plugin, [ruleId, bundleNames], pluginName) {
|
|
48
|
+
for (const bundleName of bundleNames) {
|
|
49
|
+
/* eslint-disable security/detect-object-injection */
|
|
50
|
+
if (plugin.configs[bundleName])
|
|
51
|
+
plugin.configs[bundleName].rules[`${pluginName}/${ruleId}`] = 'error'
|
|
52
|
+
/* eslint-enable security/detect-object-injection */
|
|
53
|
+
if (process.env.DEBUG_MODE)
|
|
54
|
+
console.warn(`[${pluginName}] '${ruleId}' will be removed from '${bundleName}' in a future major release`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
58
|
export function isContentScriptFile(context, manifest) {
|
|
59
59
|
const scripts = (manifest?.content_scripts || []).flatMap(cs => cs.js || [])
|
|
60
60
|
if (!scripts.length) return false
|
|
@@ -87,8 +87,7 @@ export function readManifest(context) {
|
|
|
87
87
|
if (!root) return null
|
|
88
88
|
if (manifestCache.has(root)) return manifestCache.get(root)
|
|
89
89
|
let parsed
|
|
90
|
-
try {
|
|
91
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
90
|
+
try { // eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
92
91
|
parsed = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8'))
|
|
93
92
|
} catch { parsed = null }
|
|
94
93
|
manifestCache.set(root, parsed)
|