eslint-plugin-chrome-extensions 2.5.0 → 2.6.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 +195 -119
- package/package.json +4 -7
- package/src/data/deprecations.js +2 -4
- package/src/data/i18n.js +8 -0
- package/src/data/permissions.js +1 -8
- package/src/data/regex.js +1 -1
- package/src/index.js +1 -1
- package/src/rules/no-invalid-locales.js +89 -0
- package/src/rules/no-invalid-permissions.js +3 -5
- package/src/rules/no-missing-default-locale.js +39 -14
- package/src/rules/no-missing-i18n-keys.js +3 -5
- package/src/rules/no-sensitive-permissions.js +1 -1
- package/src/rules/no-unrestricted-host-permissions.js +31 -16
- package/src/rules/no-unused-permissions.js +13 -4
- package/src/rules/require-mv3.js +1 -1
- package/src/rules/valid-manifest-icons.js +31 -3
- package/src/rules/valid-mv3-schema.js +1 -1
- package/src/rules/valid-url-fields.js +42 -37
- package/src/rules/valid-version-field.js +1 -1
- package/src/utils/extension.js +13 -0
- package/src/utils/i18n.js +3 -0
- package/src/utils/init.js +3 -1
- package/src/utils/license.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-chrome-extensions",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "ESLint rules for Chrome extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -54,10 +54,8 @@
|
|
|
54
54
|
"bump:minor": "bash utils/bump.sh minor",
|
|
55
55
|
"bump:patch": "bash utils/bump.sh patch",
|
|
56
56
|
"demo": "node demo/run.js",
|
|
57
|
-
"lint": "
|
|
58
|
-
"lint:all": "
|
|
59
|
-
"lint:fix": "eslint . --fix --cache && yamllint .",
|
|
60
|
-
"lint:fix-all": "eslint . --fix && yamllint .",
|
|
57
|
+
"lint": "pre-commit run",
|
|
58
|
+
"lint:all": "pre-commit run --all-files",
|
|
61
59
|
"test": "node tests/run.js"
|
|
62
60
|
},
|
|
63
61
|
"dependencies": {
|
|
@@ -83,8 +81,7 @@
|
|
|
83
81
|
"eslint-plugin-package-json": "^1.10.1",
|
|
84
82
|
"eslint-plugin-promise": "^7.3.0",
|
|
85
83
|
"eslint-plugin-regexp": "^3.3.1",
|
|
86
|
-
"eslint-plugin-security": "^4.2.0"
|
|
87
|
-
"yamllint-js": "^0.2.4"
|
|
84
|
+
"eslint-plugin-security": "^4.2.0"
|
|
88
85
|
},
|
|
89
86
|
"peerDependencies": {
|
|
90
87
|
"@eslint/css": ">=0.5.0",
|
package/src/data/deprecations.js
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
bundles: {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'valid-mv3-schema': ['recommended'], // 2026-10-5 announced future removal
|
|
8
|
-
'valid-version-field': ['recommended'] // 2026-10-5 announced future removal
|
|
5
|
+
// <rule-id-a>: [<old-bundle>],
|
|
6
|
+
// <rule-id-b>: [<old-bundle>]
|
|
9
7
|
}
|
|
10
8
|
}
|
package/src/data/i18n.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
defaultScanExtensions: ['cjs', 'css', 'htm', 'html', 'js', 'json', 'jsx', 'md', 'mjs', 'svg', 'ts', 'tsx', 'txt'],
|
|
3
|
+
locales: {
|
|
4
|
+
supported: new Set([ // https://developer.chrome.com/docs/extensions/reference/api/i18n
|
|
5
|
+
'af', 'am', 'ar', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'en_AU', 'en_GB', 'en_IN', 'en_US', 'es',
|
|
6
|
+
'es_419', 'es_MX', 'es_US', 'et', 'fa', 'fi', 'fil', 'fr', 'fr_CA', 'fr_FR', 'gu', 'he', 'hi', 'hr', 'hu',
|
|
7
|
+
'id', 'it', 'ja', 'kn', 'ko', 'lt', 'lv', 'ml', 'mr', 'ms', 'nl', 'no', 'pl', 'pt_BR', 'pt_PT', 'ro', 'ru',
|
|
8
|
+
'sk', 'sl', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW'
|
|
9
|
+
])
|
|
10
|
+
},
|
|
3
11
|
re: {
|
|
4
12
|
getMsg: {
|
|
5
13
|
literal: /\b(?:browser|chrome)\.i18n\.getMessage\(\s*['"]([^'"]+)['"]/g,
|
package/src/data/permissions.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import regex from './regex.js'
|
|
2
|
-
|
|
3
1
|
export default {
|
|
4
2
|
sensitive: new Set([
|
|
5
3
|
'accessibilityFeatures.modify', 'accessibilityFeatures.read', 'bookmarks', 'browsingData', 'clipboardRead',
|
|
@@ -22,10 +20,5 @@ export default {
|
|
|
22
20
|
'sessions', 'sidePanel', 'storage', 'system.cpu', 'system.display', 'system.memory', 'system.storage',
|
|
23
21
|
'tabCapture', 'tabGroups', 'tabs', 'topSites', 'tts', 'ttsEngine', 'unlimitedStorage', 'userScripts',
|
|
24
22
|
'vpnProvider', 'wallpaper', 'webAuthenticationProxy', 'webNavigation', 'webRequest', 'webRequestBlocking'
|
|
25
|
-
])
|
|
26
|
-
isValid(name, target = 'mv3') {
|
|
27
|
-
if (this.valid.has(name)) return true
|
|
28
|
-
if (target == 'mv3') return false
|
|
29
|
-
return regex.url.test(name) || name.startsWith('<') || name == '*://*/*'
|
|
30
|
-
}
|
|
23
|
+
])
|
|
31
24
|
}
|
package/src/data/regex.js
CHANGED
package/src/index.js
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- lint-time project inspection only */
|
|
2
|
+
|
|
3
|
+
import extension from '../utils/extension.js'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import i18nUtils from '../utils/i18n.js'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
|
|
8
|
+
import regex from '../data/regex.js'
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Disallow invalid Chrome locale codes in `_locales/` folders and `default_locale`',
|
|
15
|
+
category: 'Possible Errors',
|
|
16
|
+
recommended: true,
|
|
17
|
+
manifest: true
|
|
18
|
+
},
|
|
19
|
+
schema: [{
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
allowedLocales: {
|
|
23
|
+
oneOf: [
|
|
24
|
+
{ type: 'string', description: 'Single locale code to allow' },
|
|
25
|
+
{
|
|
26
|
+
type: 'array',
|
|
27
|
+
items: { type: 'string', description: 'Locale code to allow' },
|
|
28
|
+
description: 'List of locale codes to allow'
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
description: 'Additional locale codes to allow (e.g. for Chromium forks)'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false
|
|
35
|
+
}],
|
|
36
|
+
defaultOptions: [{ allowedLocales: [] }],
|
|
37
|
+
messages: {
|
|
38
|
+
invalidLocale: '`{{value}}` is not a Chrome-supported locale code.'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
create(context) {
|
|
43
|
+
const filename = extension.file.getName(context),
|
|
44
|
+
isManifest = regex.filename.manifest.test(filename),
|
|
45
|
+
isScript = regex.filename.script.test(filename)
|
|
46
|
+
if (!isManifest && !isScript) return {}
|
|
47
|
+
const opts = context.options[0] || {},
|
|
48
|
+
allowed = new Set(Array.isArray(opts.allowedLocales) ? opts.allowedLocales
|
|
49
|
+
: opts.allowedLocales ? [opts.allowedLocales] : []),
|
|
50
|
+
invalidFolders = [],
|
|
51
|
+
root = extension.findRoot(context)
|
|
52
|
+
if (root) {
|
|
53
|
+
const localesDir = path.join(root, '_locales')
|
|
54
|
+
try {
|
|
55
|
+
for (const entry of fs.readdirSync(localesDir, { withFileTypes: true }))
|
|
56
|
+
if (entry.isDirectory() && isInvalid(entry.name)) invalidFolders.push(entry.name)
|
|
57
|
+
invalidFolders.sort()
|
|
58
|
+
} catch {} // eslint-disable-line no-empty
|
|
59
|
+
}
|
|
60
|
+
const loc = { line: 1, column: 1 }
|
|
61
|
+
if (!isManifest) {
|
|
62
|
+
const manifest = extension.manifest.read(context)
|
|
63
|
+
if (manifest) {
|
|
64
|
+
const defaultLocale = manifest.default_locale
|
|
65
|
+
if (isInvalid(defaultLocale))
|
|
66
|
+
context.report({ loc, messageId: 'invalidLocale', data: { value: defaultLocale }})
|
|
67
|
+
}
|
|
68
|
+
for (const name of invalidFolders)
|
|
69
|
+
context.report({ loc, messageId: 'invalidLocale', data: { value: name }})
|
|
70
|
+
return {}
|
|
71
|
+
}
|
|
72
|
+
function isInvalid(locale) {
|
|
73
|
+
return typeof locale == 'string' && !i18nUtils.localeIsValid(locale, allowed) }
|
|
74
|
+
return {
|
|
75
|
+
'*'(node) {
|
|
76
|
+
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
77
|
+
const keyName = keyNode.value ?? keyNode.name ; if (keyName != 'default_locale') return
|
|
78
|
+
const valueNode = node.value ; if (valueNode?.type != 'String') return
|
|
79
|
+
if (isInvalid(valueNode.value))
|
|
80
|
+
context.report({ node: valueNode, messageId: 'invalidLocale',
|
|
81
|
+
data: { value: valueNode.value }})
|
|
82
|
+
},
|
|
83
|
+
'Document:exit'() {
|
|
84
|
+
for (const name of invalidFolders)
|
|
85
|
+
context.report({ loc, messageId: 'invalidLocale', data: { value: name }})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import extension from '../utils/extension.js'
|
|
2
|
-
|
|
3
|
-
import permissions from '../data/permissions.js'
|
|
4
2
|
import regex from '../data/regex.js'
|
|
5
3
|
|
|
6
4
|
export default {
|
|
@@ -9,7 +7,7 @@ export default {
|
|
|
9
7
|
docs: {
|
|
10
8
|
description: 'Disallow invalid or unsupported manifest permission names',
|
|
11
9
|
category: 'Possible Errors',
|
|
12
|
-
recommended:
|
|
10
|
+
recommended: true,
|
|
13
11
|
manifest: true
|
|
14
12
|
},
|
|
15
13
|
defaultOptions: [{ target: 'mv3', allowedPermissions: [] }],
|
|
@@ -46,7 +44,7 @@ export default {
|
|
|
46
44
|
: opts.allowedPermissions ? [opts.allowedPermissions] : [])
|
|
47
45
|
function checkPermission(node, permission) {
|
|
48
46
|
if (typeof permission != 'string') return
|
|
49
|
-
if (allowed.has(permission) ||
|
|
47
|
+
if (allowed.has(permission) || extension.manifest.permission.isValid(permission, target)) return
|
|
50
48
|
context.report({ node, messageId: 'invalidPermission', data: { permission, target }})
|
|
51
49
|
}
|
|
52
50
|
if (regex.filename.manifest.test(extension.file.getName(context))) // lint it directly
|
|
@@ -67,7 +65,7 @@ export default {
|
|
|
67
65
|
return {
|
|
68
66
|
'Program:exit'() {
|
|
69
67
|
for (const permission of allPermissions) {
|
|
70
|
-
if (allowed.has(permission) ||
|
|
68
|
+
if (allowed.has(permission) || extension.manifest.permission.isValid(permission, target)) continue
|
|
71
69
|
context.report({ loc: { line: 1, column: 0 },
|
|
72
70
|
messageId: 'invalidPermission', data: { permission, target }})
|
|
73
71
|
}
|
|
@@ -7,14 +7,7 @@ import path from 'node:path'
|
|
|
7
7
|
|
|
8
8
|
import regex from '../data/regex.js'
|
|
9
9
|
|
|
10
|
-
function
|
|
11
|
-
try {
|
|
12
|
-
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
13
|
-
return (data && typeof data == 'object' && !Array.isArray(data)) ? data : null
|
|
14
|
-
} catch { return null }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function readLocaleDirs(localesDir) {
|
|
10
|
+
function readLocaleDirs(localesDir) {
|
|
18
11
|
try {
|
|
19
12
|
return fs.readdirSync(localesDir, { withFileTypes: true })
|
|
20
13
|
.filter(entry => entry.isDirectory())
|
|
@@ -22,13 +15,20 @@ function readLocaleDirs(localesDir) {
|
|
|
22
15
|
} catch { return [] }
|
|
23
16
|
}
|
|
24
17
|
|
|
18
|
+
function readMessages(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
21
|
+
return (data && typeof data == 'object' && !Array.isArray(data)) ? data : null
|
|
22
|
+
} catch { return null }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
25
|
export default {
|
|
26
26
|
meta: {
|
|
27
27
|
type: 'problem',
|
|
28
28
|
docs: {
|
|
29
29
|
description: 'Ensure `default_locale` is valid and all `_locales` keys exist in it',
|
|
30
30
|
category: 'Possible Errors',
|
|
31
|
-
recommended:
|
|
31
|
+
recommended: true,
|
|
32
32
|
manifest: true
|
|
33
33
|
},
|
|
34
34
|
schema: [{
|
|
@@ -36,23 +36,29 @@ export default {
|
|
|
36
36
|
properties: {
|
|
37
37
|
ignoreKeys: {
|
|
38
38
|
type: 'array', items: { type: 'string' },
|
|
39
|
-
description: 'Keys to skip when checking
|
|
39
|
+
description: 'Keys to skip when checking locales'
|
|
40
|
+
},
|
|
41
|
+
checkAllLocales: {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
description: 'Also flag keys missing from any locale, not just the default'
|
|
40
44
|
}
|
|
41
45
|
},
|
|
42
46
|
additionalProperties: false
|
|
43
47
|
}],
|
|
44
|
-
defaultOptions: [{ ignoreKeys: [] }],
|
|
48
|
+
defaultOptions: [{ ignoreKeys: [], checkAllLocales: false }],
|
|
45
49
|
messages: {
|
|
46
50
|
missingDefaultLocale: '`_locales/` exists but `default_locale` is not declared.',
|
|
47
51
|
unexpectedDefaultLocale: '`default_locale` is declared but no `_locales/` directory exists.',
|
|
48
52
|
missingDefaultLocaleFile: '`_locales/{{locale}}/messages.json` does not exist.',
|
|
49
53
|
missingKeyInDefault: '`{{key}}` exists in `_locales/{{locale}}/messages.json`, '
|
|
50
|
-
+ 'but not in default locale `{{defaultLocale}}`.'
|
|
54
|
+
+ 'but not in default locale `{{defaultLocale}}`.',
|
|
55
|
+
missingKeyInLocale: '`{{key}}` is missing from `_locales/{{locale}}/messages.json`.'
|
|
51
56
|
}
|
|
52
57
|
},
|
|
53
58
|
|
|
54
59
|
create(context) {
|
|
55
|
-
const filename = context
|
|
60
|
+
const filename = extension.file.getName(context) ; if (!filename) return {}
|
|
61
|
+
if (!regex.filename.manifest.test(filename) && !regex.filename.script.test(filename)) return {}
|
|
56
62
|
const manifest = extension.manifest.read(context) ; if (!manifest) return {}
|
|
57
63
|
const root = extension.findRoot(context) ; if (!root) return {}
|
|
58
64
|
const localesDir = path.join(root, '_locales'),
|
|
@@ -60,6 +66,7 @@ export default {
|
|
|
60
66
|
defaultLocale = manifest.default_locale,
|
|
61
67
|
opts = context.options[0] || {},
|
|
62
68
|
ignored = new Set(opts.ignoreKeys || []),
|
|
69
|
+
checkAllLocales = opts.checkAllLocales ?? false,
|
|
63
70
|
loc = { line: 1, column: 1 }
|
|
64
71
|
if (hasLocales && typeof defaultLocale != 'string') {
|
|
65
72
|
context.report({ loc, messageId: 'missingDefaultLocale' })
|
|
@@ -75,7 +82,25 @@ export default {
|
|
|
75
82
|
context.report({ loc, messageId: 'missingDefaultLocaleFile', data: { locale: defaultLocale }})
|
|
76
83
|
return {}
|
|
77
84
|
}
|
|
78
|
-
|
|
85
|
+
const localeDirs = readLocaleDirs(localesDir)
|
|
86
|
+
if (checkAllLocales) {
|
|
87
|
+
const allMsgs = new Map([[defaultLocale, defaultMsgs]])
|
|
88
|
+
for (const locale of localeDirs) {
|
|
89
|
+
if (locale == defaultLocale) continue
|
|
90
|
+
const msgs = readMessages(path.join(localesDir, locale, 'messages.json'))
|
|
91
|
+
if (msgs) allMsgs.set(locale, msgs)
|
|
92
|
+
}
|
|
93
|
+
const union = new Set()
|
|
94
|
+
for (const msgs of allMsgs.values())
|
|
95
|
+
for (const key of Object.keys(msgs))
|
|
96
|
+
if (!i18n.isPredefined(key) && !ignored.has(key)) union.add(key)
|
|
97
|
+
for (const [locale, msgs] of allMsgs)
|
|
98
|
+
for (const key of union)
|
|
99
|
+
if (!(key in msgs))
|
|
100
|
+
context.report({ loc, messageId: 'missingKeyInLocale', data: { key, locale }})
|
|
101
|
+
return {}
|
|
102
|
+
}
|
|
103
|
+
for (const locale of localeDirs) {
|
|
79
104
|
if (locale == defaultLocale) continue
|
|
80
105
|
const msgs = readMessages(path.join(localesDir, locale, 'messages.json')) ; if (!msgs) continue
|
|
81
106
|
for (const key of Object.keys(msgs)) {
|
|
@@ -13,9 +13,7 @@ export default {
|
|
|
13
13
|
category: 'Possible Errors',
|
|
14
14
|
recommended: true,
|
|
15
15
|
manifest: true,
|
|
16
|
-
css: true
|
|
17
|
-
html: true,
|
|
18
|
-
json: true
|
|
16
|
+
css: true
|
|
19
17
|
},
|
|
20
18
|
schema: [{
|
|
21
19
|
type: 'object',
|
|
@@ -57,7 +55,7 @@ export default {
|
|
|
57
55
|
context.report({ loc, messageId: 'missingKey', data: { key, locale: i18n.locale }})
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
|
-
if (!regex.filename.
|
|
58
|
+
if (!regex.filename.css.test(filename) && !regex.filename.manifest.test(filename))
|
|
61
59
|
return {}
|
|
62
60
|
const seenKeys = new Set()
|
|
63
61
|
function scanPlaceholders() {
|
|
@@ -69,6 +67,6 @@ export default {
|
|
|
69
67
|
context.report({ loc, messageId: 'missingKey', data: { key, locale: i18n.locale }})
|
|
70
68
|
}
|
|
71
69
|
}
|
|
72
|
-
return { 'Document': scanPlaceholders, 'Program': scanPlaceholders }
|
|
70
|
+
return { 'Document': scanPlaceholders, 'Program': scanPlaceholders, 'StyleSheet': scanPlaceholders }
|
|
73
71
|
}
|
|
74
72
|
}
|
|
@@ -10,7 +10,8 @@ export default {
|
|
|
10
10
|
docs: {
|
|
11
11
|
description: 'Disallow broad host permissions that grant access to every site',
|
|
12
12
|
category: 'Possible Errors',
|
|
13
|
-
recommended:
|
|
13
|
+
recommended: true,
|
|
14
|
+
security: true,
|
|
14
15
|
manifest: true
|
|
15
16
|
},
|
|
16
17
|
defaultOptions: [{ allowedPatterns: [] }],
|
|
@@ -32,23 +33,37 @@ export default {
|
|
|
32
33
|
},
|
|
33
34
|
|
|
34
35
|
create(context) {
|
|
35
|
-
if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
|
|
36
36
|
const allowedPatterns = new Set(context.options[0]?.allowedPatterns || [])
|
|
37
|
+
function isBroad(pattern) {
|
|
38
|
+
return typeof pattern == 'string'
|
|
39
|
+
&& !allowedPatterns.has(pattern)
|
|
40
|
+
&& re.broadHost.test(pattern)
|
|
41
|
+
}
|
|
42
|
+
if (regex.filename.manifest.test(extension.file.getName(context))) // lint it directly
|
|
43
|
+
return {
|
|
44
|
+
'*'(node) {
|
|
45
|
+
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
46
|
+
const keyName = keyNode.value ?? keyNode.name ; if (!hostKeys.has(keyName)) return
|
|
47
|
+
const arrNode = node.value ; if (arrNode?.type != 'Array') return
|
|
48
|
+
const nodeElems = arrNode.elements || arrNode.items || []
|
|
49
|
+
for (const elem of nodeElems) {
|
|
50
|
+
const valueNode = elem.value || elem,
|
|
51
|
+
pattern = valueNode?.value
|
|
52
|
+
if (!isBroad(pattern)) continue
|
|
53
|
+
context.report({ node: valueNode, messageId: 'unrestrictedHost', data: { pattern }})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const manifest = extension.manifest.read(context) ; if (!manifest) return {}
|
|
37
58
|
return {
|
|
38
|
-
'
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
pattern = valueNode?.value
|
|
47
|
-
if (typeof pattern != 'string'
|
|
48
|
-
|| allowedPatterns.has(pattern)
|
|
49
|
-
|| !re.broadHost.test(pattern)
|
|
50
|
-
) continue
|
|
51
|
-
context.report({ node: valueNode, messageId: 'unrestrictedHost', data: { pattern }})
|
|
59
|
+
'Program:exit'() {
|
|
60
|
+
for (const key of hostKeys) {
|
|
61
|
+
const patterns = manifest[key] // eslint-disable-line security/detect-object-injection
|
|
62
|
+
if (!Array.isArray(patterns)) continue
|
|
63
|
+
for (const pattern of patterns)
|
|
64
|
+
if (isBroad(pattern))
|
|
65
|
+
context.report({ loc: { line: 1, column: 1 },
|
|
66
|
+
messageId: 'unrestrictedHost', data: { pattern }})
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
}
|
|
@@ -33,7 +33,7 @@ export default {
|
|
|
33
33
|
docs: {
|
|
34
34
|
description: 'Flag manifest permissions whose APIs are never used',
|
|
35
35
|
category: 'Possible Errors',
|
|
36
|
-
recommended:
|
|
36
|
+
recommended: true,
|
|
37
37
|
manifest: true
|
|
38
38
|
},
|
|
39
39
|
schema: [],
|
|
@@ -43,11 +43,11 @@ export default {
|
|
|
43
43
|
},
|
|
44
44
|
|
|
45
45
|
create(context) {
|
|
46
|
-
|
|
46
|
+
const filename = extension.file.getName(context)
|
|
47
|
+
if (!regex.filename.manifest.test(filename) && !regex.filename.script.test(filename)) return {}
|
|
47
48
|
const manifest = extension.manifest.read(context) ; if (!manifest) return {}
|
|
48
49
|
const root = extension.findRoot(context) ; if (!root) return {}
|
|
49
|
-
const usedAPIs = collectUsedAPIs(manifest, root)
|
|
50
|
-
permissionNodes = new Map()
|
|
50
|
+
const usedAPIs = collectUsedAPIs(manifest, root)
|
|
51
51
|
function isUsed(permission) {
|
|
52
52
|
let knownPerm = false
|
|
53
53
|
for (const [, perm] of apis.permissionMap)
|
|
@@ -57,6 +57,15 @@ export default {
|
|
|
57
57
|
if (apis.findPermission(api) == permission) return true
|
|
58
58
|
return false
|
|
59
59
|
}
|
|
60
|
+
if (!regex.filename.manifest.test(filename)) {
|
|
61
|
+
const loc = { line: 1, column: 1 },
|
|
62
|
+
declared = [...(manifest.permissions || []), ...(manifest.optional_permissions || [])]
|
|
63
|
+
for (const permission of declared)
|
|
64
|
+
if (typeof permission == 'string' && !isUsed(permission))
|
|
65
|
+
context.report({ loc, messageId: 'unusedPermission', data: { permission }})
|
|
66
|
+
return {}
|
|
67
|
+
}
|
|
68
|
+
const permissionNodes = new Map()
|
|
60
69
|
return {
|
|
61
70
|
'*'(node) {
|
|
62
71
|
const keyNode = node.name || node.key ; if (!keyNode) return
|
package/src/rules/require-mv3.js
CHANGED
|
@@ -7,13 +7,32 @@ import regex from '../data/regex.js'
|
|
|
7
7
|
const re = { svg: /\.svg$/i },
|
|
8
8
|
sizes = { recommended: ['16', '32', '48', '128'], required: '128' }
|
|
9
9
|
|
|
10
|
+
function validateIcons(context, icons, root, opts, loc) {
|
|
11
|
+
const { requireRecommended, checkFiles, checkSVG } = opts,
|
|
12
|
+
declaredSizes = new Set(Object.keys(icons))
|
|
13
|
+
if (requireRecommended) {
|
|
14
|
+
for (const size of sizes.recommended)
|
|
15
|
+
if (!declaredSizes.has(size))
|
|
16
|
+
context.report({ loc, messageId: 'missingSize', data: { size }})
|
|
17
|
+
} else if (!declaredSizes.has(sizes.required))
|
|
18
|
+
context.report({ loc, messageId: 'missingRequiredSize' })
|
|
19
|
+
for (const file of Object.values(icons)) {
|
|
20
|
+
if (typeof file != 'string') continue
|
|
21
|
+
if (checkSVG && re.svg.test(file))
|
|
22
|
+
context.report({ loc, messageId: 'svgNotSupported' })
|
|
23
|
+
else if (checkFiles && root // eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
24
|
+
&& !fs.existsSync(path.resolve(root, file))
|
|
25
|
+
) context.report({ loc, messageId: 'missingFile', data: { file }})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
export default {
|
|
11
30
|
meta: {
|
|
12
31
|
type: 'problem',
|
|
13
32
|
docs: {
|
|
14
33
|
description: 'Validate manifest icons: sizes, existing files, no SVG',
|
|
15
34
|
category: 'Possible Errors',
|
|
16
|
-
recommended:
|
|
35
|
+
recommended: true,
|
|
17
36
|
manifest: true
|
|
18
37
|
},
|
|
19
38
|
defaultOptions: [{
|
|
@@ -48,12 +67,21 @@ export default {
|
|
|
48
67
|
},
|
|
49
68
|
|
|
50
69
|
create(context) {
|
|
51
|
-
|
|
70
|
+
const filename = extension.file.getName(context)
|
|
71
|
+
if (!regex.filename.manifest.test(filename) && !regex.filename.script.test(filename)) return {}
|
|
52
72
|
const opts = context.options[0] || {},
|
|
53
73
|
requireRecommended = opts.requireRecommendedSizes !== false,
|
|
54
74
|
checkFiles = opts.checkIconExists !== false,
|
|
55
75
|
checkSVG = opts.noSVG !== false,
|
|
56
|
-
root = checkFiles ? extension.findRoot(context) : null
|
|
76
|
+
root = checkFiles ? extension.findRoot(context) : null,
|
|
77
|
+
resolved = { requireRecommended, checkFiles, checkSVG }
|
|
78
|
+
if (!regex.filename.manifest.test(filename)) {
|
|
79
|
+
const manifest = extension.manifest.read(context) ; if (!manifest) return {}
|
|
80
|
+
const icons = manifest.icons
|
|
81
|
+
if (!icons || typeof icons != 'object' || Array.isArray(icons)) return {}
|
|
82
|
+
validateIcons(context, icons, root, resolved, { line: 1, column: 1 })
|
|
83
|
+
return {}
|
|
84
|
+
}
|
|
57
85
|
return {
|
|
58
86
|
'*'(node) {
|
|
59
87
|
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
@@ -22,6 +22,39 @@ function collectStrings(obj, path = [], out = []) {
|
|
|
22
22
|
|
|
23
23
|
function isURL(value) { return re.absoluteURL.test(value.replace(re.template, 'x')) }
|
|
24
24
|
|
|
25
|
+
function validateManifest(context, manifest, opts) {
|
|
26
|
+
const { checkURLfields, checkMatchPatterns, allowedSchemes, allowAllURLs } = opts,
|
|
27
|
+
loc = { line: 1, column: 1 }
|
|
28
|
+
for (const { path, value } of collectStrings(manifest)) {
|
|
29
|
+
if (checkURLfields && manifestData.paths.url.has(path)) {
|
|
30
|
+
if (!isURL(value))
|
|
31
|
+
context.report({ loc, messageId: 'invalidURL', data: { field: path, value }})
|
|
32
|
+
} else if (checkMatchPatterns && manifestData.paths.match.has(path)) {
|
|
33
|
+
if (value == '<all_urls>') {
|
|
34
|
+
if (!allowAllURLs)
|
|
35
|
+
context.report({ loc, messageId: 'allURLs', data: { field: path }})
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
const match = value.match(re.match)
|
|
39
|
+
if (!match) {
|
|
40
|
+
context.report({ loc, messageId: 'invalidMatchPattern', data: { field: path, value }})
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
const [, scheme, host] = match
|
|
44
|
+
if (scheme != '*' && !allowedSchemes.includes(scheme))
|
|
45
|
+
context.report({ loc, messageId: 'invalidScheme', data: { field: path, value }})
|
|
46
|
+
else if (scheme == 'file' && host != '')
|
|
47
|
+
context.report({ loc, messageId: 'invalidFileHost', data: { field: path, value }})
|
|
48
|
+
else if (path == 'externally_connectable.matches' && host.includes('*'))
|
|
49
|
+
context.report({ loc, messageId: 'invalidExternalHost', data: { field: path, value }})
|
|
50
|
+
else if (host.includes('*') && host != '*' && !host.startsWith('*.'))
|
|
51
|
+
context.report({ loc, messageId: 'invalidHost', data: { field: path, value }})
|
|
52
|
+
else if (path == 'web_accessible_resources.matches' && !value.endsWith('/*'))
|
|
53
|
+
context.report({ loc, messageId: 'invalidWARPath', data: { field: path, value }})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
25
58
|
export default {
|
|
26
59
|
meta: {
|
|
27
60
|
type: 'problem',
|
|
@@ -70,53 +103,25 @@ export default {
|
|
|
70
103
|
},
|
|
71
104
|
|
|
72
105
|
create(context) {
|
|
73
|
-
|
|
106
|
+
const filename = extension.file.getName(context)
|
|
107
|
+
if (!regex.filename.manifest.test(filename) && !regex.filename.script.test(filename)) return {}
|
|
74
108
|
const opts = context.options[0] || {},
|
|
75
109
|
checkURLfields = opts.checkURLfields ?? true,
|
|
76
110
|
checkMatchPatterns = opts.checkMatchPatterns ?? true,
|
|
77
111
|
allowedSchemes = opts.allowedSchemes ?? ['https', 'file'],
|
|
78
112
|
allowAllURLs = opts.allowAllURLs ?? false
|
|
79
113
|
if (!checkURLfields && !checkMatchPatterns) return {}
|
|
114
|
+
const resolved = { checkURLfields, checkMatchPatterns, allowedSchemes, allowAllURLs }
|
|
115
|
+
if (!regex.filename.manifest.test(filename)) {
|
|
116
|
+
const manifest = extension.manifest.read(context) ; if (!manifest) return {}
|
|
117
|
+
validateManifest(context, manifest, resolved)
|
|
118
|
+
return {}
|
|
119
|
+
}
|
|
80
120
|
return {
|
|
81
121
|
'Document:exit'() {
|
|
82
122
|
let manifest
|
|
83
123
|
try { manifest = JSON.parse(context.sourceCode.text) } catch { return }
|
|
84
|
-
|
|
85
|
-
if (checkURLfields && manifestData.paths.url.has(path)) {
|
|
86
|
-
if (!isURL(value))
|
|
87
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidURL',
|
|
88
|
-
data: { field: path, value }})
|
|
89
|
-
} else if (checkMatchPatterns && manifestData.paths.match.has(path)) {
|
|
90
|
-
if (value == '<all_urls>') {
|
|
91
|
-
if (!allowAllURLs)
|
|
92
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'allURLs',
|
|
93
|
-
data: { field: path }})
|
|
94
|
-
continue
|
|
95
|
-
}
|
|
96
|
-
const match = value.match(re.match)
|
|
97
|
-
if (!match) {
|
|
98
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidMatchPattern',
|
|
99
|
-
data: { field: path, value }})
|
|
100
|
-
continue
|
|
101
|
-
}
|
|
102
|
-
const [, scheme, host] = match
|
|
103
|
-
if (scheme != '*' && !allowedSchemes.includes(scheme))
|
|
104
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidScheme',
|
|
105
|
-
data: { field: path, value }})
|
|
106
|
-
else if (scheme == 'file' && host != '')
|
|
107
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidFileHost',
|
|
108
|
-
data: { field: path, value }})
|
|
109
|
-
else if (path == 'externally_connectable.matches' && host.includes('*'))
|
|
110
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidExternalHost',
|
|
111
|
-
data: { field: path, value }})
|
|
112
|
-
else if (host.includes('*') && host != '*' && !host.startsWith('*.'))
|
|
113
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidHost',
|
|
114
|
-
data: { field: path, value }})
|
|
115
|
-
else if (path == 'web_accessible_resources.matches' && !value.endsWith('/*'))
|
|
116
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidWARPath',
|
|
117
|
-
data: { field: path, value }})
|
|
118
|
-
}
|
|
119
|
-
}
|
|
124
|
+
validateManifest(context, manifest, resolved)
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
}
|