eslint-plugin-chrome-extensions 2.8.1 → 2.9.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 +73 -80
- package/package.json +5 -3
- package/src/rules/css-spacing.js +9 -14
- package/src/rules/manifest-key-order.js +6 -5
- package/src/rules/manifest-spacing.js +5 -8
- package/src/rules/no-content-style-leak.js +4 -3
- package/src/rules/no-deprecated-apis.js +6 -6
- package/src/rules/no-global-keyframes.js +5 -8
- package/src/rules/no-host-page-selectors.js +12 -16
- package/src/rules/no-inline-event-handlers.js +7 -5
- package/src/rules/no-inline-scripts.js +10 -6
- package/src/rules/no-insecure-fetch.js +10 -6
- package/src/rules/no-insecure-messaging.js +8 -10
- package/src/rules/no-invalid-locales.js +7 -6
- package/src/rules/no-invalid-permissions.js +5 -5
- package/src/rules/no-missing-default-locale.js +5 -4
- package/src/rules/no-missing-i18n-keys.js +7 -5
- package/src/rules/no-missing-permissions.js +1 -1
- package/src/rules/no-missing-web-accessible-resources.js +10 -10
- package/src/rules/no-remote-code.js +30 -33
- package/src/rules/no-remote-objects.js +7 -6
- package/src/rules/no-remote-urls.js +11 -12
- package/src/rules/no-scrollbar-styling.js +6 -6
- package/src/rules/no-sensitive-permissions.js +6 -4
- package/src/rules/no-unrestricted-host-permissions.js +6 -7
- package/src/rules/no-unused-i18n-keys.js +5 -4
- package/src/rules/require-short-name-when-useful.js +3 -8
- package/src/rules/valid-filenames.js +9 -11
- package/src/rules/valid-license-file.js +7 -6
- package/src/rules/valid-manifest-icons.js +14 -20
- package/src/rules/valid-service-worker.js +8 -10
- package/src/rules/valid-url-fields.js +8 -13
- package/src/rules/valid-version-field.js +5 -6
- package/src/utils/extension.js +7 -3
- package/src/utils/init.js +5 -4
- package/src/utils/node.js +23 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import api from '../utils/api.js'
|
|
2
2
|
import nodeUtils from '../utils/node.js'
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const defaultOptions = { disallowWildcardTarget: true },
|
|
5
|
+
re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ },
|
|
5
6
|
isMessageTarget = obj => obj?.type == 'Identifier' && ['self', 'window'].includes(obj.name)
|
|
6
7
|
|
|
7
8
|
export default {
|
|
@@ -13,7 +14,7 @@ export default {
|
|
|
13
14
|
recommended: true,
|
|
14
15
|
security: true
|
|
15
16
|
},
|
|
16
|
-
defaultOptions: [
|
|
17
|
+
defaultOptions: [defaultOptions],
|
|
17
18
|
schema: [{
|
|
18
19
|
type: 'object',
|
|
19
20
|
properties: {
|
|
@@ -33,8 +34,7 @@ export default {
|
|
|
33
34
|
},
|
|
34
35
|
|
|
35
36
|
create(context) {
|
|
36
|
-
const
|
|
37
|
-
disallowWildcard = opts.disallowWildcardTarget !== false
|
|
37
|
+
const { disallowWildcardTarget } = { ...defaultOptions, ...context.options[0] }
|
|
38
38
|
function checkPostMessageHandler(node, handlerNode, target) {
|
|
39
39
|
if (!handlerNode) return
|
|
40
40
|
const body = context.sourceCode.getText(handlerNode)
|
|
@@ -59,14 +59,12 @@ export default {
|
|
|
59
59
|
context.report({ node, messageId: 'unfilteredBroadcast' })
|
|
60
60
|
}
|
|
61
61
|
if (node.callee.property?.name == 'addEventListener'
|
|
62
|
-
&& node.arguments[0]
|
|
62
|
+
&& nodeUtils.getStringLiteral(node.arguments[0]) == 'message'
|
|
63
63
|
&& isMessageTarget(node.callee.object)
|
|
64
64
|
) checkPostMessageHandler(node, node.arguments[1], node.callee.object.name)
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
context.report({ node: target, messageId: 'wildcardTarget' })
|
|
69
|
-
}
|
|
65
|
+
if (disallowWildcardTarget && node.callee.property?.name == 'postMessage'
|
|
66
|
+
&& nodeUtils.getStringLiteral(node.arguments[1]) == '*'
|
|
67
|
+
) context.report({ node: node.arguments[1], messageId: 'wildcardTarget' })
|
|
70
68
|
},
|
|
71
69
|
AssignmentExpression(node) {
|
|
72
70
|
if (node.left.type == 'MemberExpression'
|
|
@@ -5,6 +5,8 @@ import path from 'node:path'
|
|
|
5
5
|
|
|
6
6
|
import regex from '../data/regex.js'
|
|
7
7
|
|
|
8
|
+
const defaultOptions = { allowedLocales: [] }
|
|
9
|
+
|
|
8
10
|
export default {
|
|
9
11
|
meta: {
|
|
10
12
|
type: 'problem',
|
|
@@ -31,7 +33,7 @@ export default {
|
|
|
31
33
|
},
|
|
32
34
|
additionalProperties: false
|
|
33
35
|
}],
|
|
34
|
-
defaultOptions: [
|
|
36
|
+
defaultOptions: [defaultOptions],
|
|
35
37
|
messages: {
|
|
36
38
|
invalidLocale: '`{{value}}` is not a Chrome-supported locale code.'
|
|
37
39
|
}
|
|
@@ -42,11 +44,10 @@ export default {
|
|
|
42
44
|
isManifest = regex.filename.manifest.test(filename),
|
|
43
45
|
isScript = regex.filename.script.test(filename)
|
|
44
46
|
if (!isManifest && !isScript) return {}
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
root = extension.findRoot(context)
|
|
47
|
+
const { allowedLocales } = { ...defaultOptions, ...context.options[0] },
|
|
48
|
+
allowed = new Set(Array.isArray(allowedLocales) ? allowedLocales : [allowedLocales]),
|
|
49
|
+
invalidFolders = [],
|
|
50
|
+
root = extension.findRoot(context)
|
|
50
51
|
if (root) {
|
|
51
52
|
const localesDir = path.join(root, '_locales')
|
|
52
53
|
try { // oxlint-disable-next-line security/detect-non-literal-fs-filename
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import extension from '../utils/extension.js'
|
|
2
2
|
import regex from '../data/regex.js'
|
|
3
3
|
|
|
4
|
+
const defaultOptions = { target: 'mv3', allowedPermissions: [] }
|
|
5
|
+
|
|
4
6
|
export default {
|
|
5
7
|
meta: {
|
|
6
8
|
type: 'problem',
|
|
@@ -10,7 +12,7 @@ export default {
|
|
|
10
12
|
recommended: true,
|
|
11
13
|
manifest: true
|
|
12
14
|
},
|
|
13
|
-
defaultOptions: [
|
|
15
|
+
defaultOptions: [defaultOptions],
|
|
14
16
|
schema: [{
|
|
15
17
|
type: 'object',
|
|
16
18
|
properties: {
|
|
@@ -38,10 +40,8 @@ export default {
|
|
|
38
40
|
},
|
|
39
41
|
|
|
40
42
|
create(context) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
allowed = new Set(Array.isArray(opts.allowedPermissions) ? opts.allowedPermissions
|
|
44
|
-
: opts.allowedPermissions ? [opts.allowedPermissions] : [])
|
|
43
|
+
const { target, allowedPermissions } = { ...defaultOptions, ...context.options[0] },
|
|
44
|
+
allowed = new Set(Array.isArray(allowedPermissions) ? allowedPermissions : [allowedPermissions])
|
|
45
45
|
function checkPermission(node, permission) {
|
|
46
46
|
if (typeof permission != 'string') return
|
|
47
47
|
if (allowed.has(permission) || extension.manifest.permission.isValid(permission, target)) return
|
|
@@ -7,6 +7,8 @@ import path from 'node:path'
|
|
|
7
7
|
|
|
8
8
|
import regex from '../data/regex.js'
|
|
9
9
|
|
|
10
|
+
const defaultOptions = { ignoreKeys: [], checkAllLocales: false }
|
|
11
|
+
|
|
10
12
|
function readLocaleDirs(localesDir) {
|
|
11
13
|
try {
|
|
12
14
|
return fs.readdirSync(localesDir, { withFileTypes: true })
|
|
@@ -45,7 +47,7 @@ export default {
|
|
|
45
47
|
},
|
|
46
48
|
additionalProperties: false
|
|
47
49
|
}],
|
|
48
|
-
defaultOptions: [
|
|
50
|
+
defaultOptions: [defaultOptions],
|
|
49
51
|
messages: {
|
|
50
52
|
missingDefaultLocale: '`_locales/` exists but `default_locale` is not declared.',
|
|
51
53
|
unexpectedDefaultLocale: '`default_locale` is declared but no `_locales/` directory exists.',
|
|
@@ -64,9 +66,8 @@ export default {
|
|
|
64
66
|
const localesDir = path.join(root, '_locales'),
|
|
65
67
|
hasLocales = fs.existsSync(localesDir),
|
|
66
68
|
defaultLocale = manifest.default_locale,
|
|
67
|
-
|
|
68
|
-
ignored = new Set(
|
|
69
|
-
checkAllLocales = opts.checkAllLocales ?? false,
|
|
69
|
+
{ ignoreKeys, checkAllLocales } = { ...defaultOptions, ...context.options[0] },
|
|
70
|
+
ignored = new Set(ignoreKeys),
|
|
70
71
|
loc = { line: 1, column: 1 }
|
|
71
72
|
if (hasLocales && typeof defaultLocale != 'string') {
|
|
72
73
|
context.report({ loc, messageId: 'missingDefaultLocale' })
|
|
@@ -5,6 +5,8 @@ import nodeUtils from '../utils/node.js'
|
|
|
5
5
|
import i18nData from '../data/i18n.js'
|
|
6
6
|
import regex from '../data/regex.js'
|
|
7
7
|
|
|
8
|
+
const defaultOptions = { ignoreKeys: [] }
|
|
9
|
+
|
|
8
10
|
export default {
|
|
9
11
|
meta: {
|
|
10
12
|
type: 'problem',
|
|
@@ -25,7 +27,7 @@ export default {
|
|
|
25
27
|
},
|
|
26
28
|
additionalProperties: false
|
|
27
29
|
}],
|
|
28
|
-
defaultOptions: [
|
|
30
|
+
defaultOptions: [defaultOptions],
|
|
29
31
|
messages: {
|
|
30
32
|
missingKey: '`{{key}}` is not in `_locales/{{locale}}/messages.json`.'
|
|
31
33
|
}
|
|
@@ -36,10 +38,10 @@ export default {
|
|
|
36
38
|
const manifest = extension.manifest.read(context) ; if (!manifest?.default_locale) return {}
|
|
37
39
|
const root = extension.findRoot(context) ; if (!root) return {}
|
|
38
40
|
const i18n = i18nUtils.readDefaultLocaleMessages(manifest, root) ; if (!i18n) return {}
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const { ignoreKeys } = { ...defaultOptions, ...context.options[0] },
|
|
42
|
+
ignored = new Set(ignoreKeys),
|
|
43
|
+
isMissing = key => !i18nUtils.isPredefined(key) && !ignored.has(key) && !(key in i18n.messages),
|
|
44
|
+
loc = { line: 1, column: 1 }
|
|
43
45
|
if (regex.filename.script.test(filename))
|
|
44
46
|
return {
|
|
45
47
|
CallExpression(node) {
|
|
@@ -7,7 +7,7 @@ function queriesActiveTab(node) {
|
|
|
7
7
|
return arg.properties.some(prop => {
|
|
8
8
|
if (prop.type != 'Property') return false
|
|
9
9
|
const key = prop.key?.name ?? prop.key?.value
|
|
10
|
-
return key == 'active' && prop.value?.
|
|
10
|
+
return key == 'active' && nodeUtils.getLiteral(prop.value)?.value === true
|
|
11
11
|
})
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -8,13 +8,12 @@ function collectWARpatterns(manifest) {
|
|
|
8
8
|
if (!Array.isArray(war)) return []
|
|
9
9
|
const patterns = []
|
|
10
10
|
for (const entry of war) {
|
|
11
|
-
if (typeof entry == 'string') patterns.push(entry)
|
|
11
|
+
if (typeof entry == 'string') patterns.push(entry)
|
|
12
12
|
else if (Array.isArray(entry?.resources))
|
|
13
|
-
for (const res of entry.resources) patterns.push(res)
|
|
13
|
+
for (const res of entry.resources) patterns.push(res)
|
|
14
14
|
}
|
|
15
15
|
return patterns
|
|
16
16
|
}
|
|
17
|
-
|
|
18
17
|
function patternToRegex(pattern) {
|
|
19
18
|
const escaped = pattern.replaceAll(re.regexSpecial, '\\$&').replaceAll('*', '.*')
|
|
20
19
|
return new RegExp(`^${escaped}$`) // oxlint-disable-line security/detect-non-literal-regexp
|
|
@@ -29,9 +28,7 @@ export default {
|
|
|
29
28
|
recommended: true
|
|
30
29
|
},
|
|
31
30
|
schema: [],
|
|
32
|
-
messages: {
|
|
33
|
-
missingWAR: '`{{path}}` is not in `web_accessible_resources`.'
|
|
34
|
-
}
|
|
31
|
+
messages: { missingWAR: '`{{path}}` is not in `web_accessible_resources`.' }
|
|
35
32
|
},
|
|
36
33
|
|
|
37
34
|
create(context) {
|
|
@@ -43,10 +40,13 @@ export default {
|
|
|
43
40
|
if (node.callee.type != 'MemberExpression'
|
|
44
41
|
|| nodeUtils.getMemberPath(node.callee) != 'chrome.runtime.getURL'
|
|
45
42
|
) return
|
|
46
|
-
const arg = node.arguments[0]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
const arg = node.arguments[0],
|
|
44
|
+
val = nodeUtils.getStringLiteral(arg)
|
|
45
|
+
if (val === null) return
|
|
46
|
+
const resourcePath = val.replace(re.leadingSlash, '')
|
|
47
|
+
for (const regex of regexes)
|
|
48
|
+
if (regex.test(resourcePath)) return
|
|
49
|
+
context.report({ node: arg, messageId: 'missingWAR', data: { path: val }})
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -3,22 +3,21 @@ import nodeUtils from '../utils/node.js'
|
|
|
3
3
|
import manifest from '../data/manifest.js'
|
|
4
4
|
import regex from '../data/regex.js'
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const defaultOptions = { patterns: manifest.patterns.remoteCode, checkExecuteScriptClosures: true },
|
|
7
|
+
re = { inlineHandler: /^on[a-z]+$/i, inlineHandlerInHTML: /\son[a-z]+\s*=/i }
|
|
7
8
|
|
|
8
9
|
function isCreateScript(node) {
|
|
9
10
|
return node?.type == 'CallExpression'
|
|
10
11
|
&& node.callee?.property?.name == 'createElement'
|
|
11
|
-
&& node.arguments[0]
|
|
12
|
+
&& nodeUtils.getStringLiteral(node.arguments[0]) == 'script'
|
|
12
13
|
}
|
|
13
14
|
function isInlineHandlerInString(node) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
&& re.inlineHandlerInHTML.test(node.value)
|
|
15
|
+
const value = nodeUtils.getStringLiteral(node)
|
|
16
|
+
return value !== null && re.inlineHandlerInHTML.test(value)
|
|
17
17
|
}
|
|
18
18
|
function isLiteralURL(node) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
&& regex.url.test(node.value)
|
|
19
|
+
const value = nodeUtils.getStringLiteral(node)
|
|
20
|
+
return value !== null && regex.url.test(value)
|
|
22
21
|
}
|
|
23
22
|
function isObjectAssign(node) {
|
|
24
23
|
return node?.type == 'CallExpression'
|
|
@@ -44,10 +43,7 @@ export default {
|
|
|
44
43
|
recommended: true,
|
|
45
44
|
security: true
|
|
46
45
|
},
|
|
47
|
-
defaultOptions: [
|
|
48
|
-
patterns: manifest.patterns.remoteCode,
|
|
49
|
-
checkExecuteScriptClosures: true
|
|
50
|
-
}],
|
|
46
|
+
defaultOptions: [defaultOptions],
|
|
51
47
|
schema: [{
|
|
52
48
|
type: 'object',
|
|
53
49
|
properties: {
|
|
@@ -77,10 +73,9 @@ export default {
|
|
|
77
73
|
},
|
|
78
74
|
|
|
79
75
|
create(context) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
scriptElems = new Set()
|
|
76
|
+
const { patterns: rawPatterns, checkExecuteScriptClosures } = { ...defaultOptions, ...context.options[0] },
|
|
77
|
+
patterns = new Set(rawPatterns),
|
|
78
|
+
scriptElems = new Set()
|
|
84
79
|
function isScriptSrcAssignment(left, right) {
|
|
85
80
|
if (left.type != 'MemberExpression') return false
|
|
86
81
|
if (left.property?.name != 'src') return false
|
|
@@ -97,7 +92,7 @@ export default {
|
|
|
97
92
|
}
|
|
98
93
|
return {
|
|
99
94
|
VariableDeclarator(node) {
|
|
100
|
-
if (isCreateScript(node.init) && node.id.type == 'Identifier')
|
|
95
|
+
if (isCreateScript(nodeUtils.unwrapTS(node.init)) && node.id.type == 'Identifier')
|
|
101
96
|
scriptElems.add(node.id.name)
|
|
102
97
|
},
|
|
103
98
|
ImportExpression(node) {
|
|
@@ -113,8 +108,7 @@ export default {
|
|
|
113
108
|
if (patterns.has('string-timer')
|
|
114
109
|
&& node.callee.type == 'Identifier'
|
|
115
110
|
&& ['setTimeout', 'setInterval'].includes(node.callee.name)
|
|
116
|
-
&& node.arguments[0]
|
|
117
|
-
&& typeof node.arguments[0].value == 'string'
|
|
111
|
+
&& nodeUtils.getStringLiteral(node.arguments[0]) !== null
|
|
118
112
|
) context.report({ node, messageId: 'stringTimer', data: { fn: node.callee.name }})
|
|
119
113
|
if (patterns.has('dynamic-import')
|
|
120
114
|
&& node.callee.type == 'Import'
|
|
@@ -130,16 +124,17 @@ export default {
|
|
|
130
124
|
hasArgs = optsNode.properties.some(prop => prop.key?.name == 'args')
|
|
131
125
|
if (patterns.has('execute-script-url') && urlProp)
|
|
132
126
|
context.report({ node: urlProp, messageId: 'executeScriptURL' })
|
|
133
|
-
if (
|
|
127
|
+
if (checkExecuteScriptClosures && funcProp && !hasArgs)
|
|
134
128
|
context.report({ node: funcProp, messageId: 'executeScriptClosure' })
|
|
135
129
|
}
|
|
136
130
|
if (patterns.has('inline-event-handler')
|
|
137
131
|
&& node.callee.property?.name == 'setAttribute'
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
&& re.inlineHandler.test(
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
) {
|
|
133
|
+
const attr = nodeUtils.getStringLiteral(node.arguments[0])
|
|
134
|
+
if (attr !== null && re.inlineHandler.test(attr))
|
|
135
|
+
context.report({ node, messageId: 'inlineEventHandler',
|
|
136
|
+
data: { attr: attr.toLowerCase() }})
|
|
137
|
+
}
|
|
143
138
|
if (patterns.has('inline-event-handler')
|
|
144
139
|
&& node.callee.property?.name == 'insertAdjacentHTML'
|
|
145
140
|
&& isInlineHandlerInString(node.arguments[1])
|
|
@@ -148,10 +143,11 @@ export default {
|
|
|
148
143
|
if (patterns.has('inline-script-body')
|
|
149
144
|
&& ['append', 'prepend'].includes(node.callee.property?.name)
|
|
150
145
|
&& isScriptElement(node.callee.object)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
&&
|
|
154
|
-
|
|
146
|
+
) {
|
|
147
|
+
const body = nodeUtils.getStringLiteral(node.arguments[0])
|
|
148
|
+
if (body !== null && body.trim().length > 0)
|
|
149
|
+
context.report({ node: node.arguments[0], messageId: 'inlineScriptBody' })
|
|
150
|
+
}
|
|
155
151
|
}
|
|
156
152
|
},
|
|
157
153
|
NewExpression(node) {
|
|
@@ -170,10 +166,11 @@ export default {
|
|
|
170
166
|
&& node.left.type == 'MemberExpression'
|
|
171
167
|
&& ['textContent', 'innerText', 'innerHTML', 'text'].includes(node.left.property?.name)
|
|
172
168
|
&& isScriptElement(node.left.object)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
&&
|
|
176
|
-
|
|
169
|
+
) {
|
|
170
|
+
const body = nodeUtils.getStringLiteral(node.right)
|
|
171
|
+
if (body !== null && body.trim().length > 0)
|
|
172
|
+
context.report({ node, messageId: 'inlineScriptBody' })
|
|
173
|
+
}
|
|
177
174
|
if (!patterns.has('remote-script-src')) return
|
|
178
175
|
if (isScriptSrcAssignment(node.left, node.right))
|
|
179
176
|
context.report({ node: node.right, messageId: 'remoteScriptSrc' })
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import array from '../utils/array.js'
|
|
2
2
|
import html from '../utils/html.js'
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const defaultOptions = { allowedPrefixes: ['chrome-extension://'] },
|
|
5
|
+
re = { objectTag: /<(?:object|embed)\b([^>]*)>/gi }
|
|
5
6
|
|
|
6
7
|
export default {
|
|
7
8
|
meta: {
|
|
@@ -11,7 +12,7 @@ export default {
|
|
|
11
12
|
category: 'Possible Errors',
|
|
12
13
|
html: true
|
|
13
14
|
},
|
|
14
|
-
defaultOptions: [
|
|
15
|
+
defaultOptions: [defaultOptions],
|
|
15
16
|
schema: [{
|
|
16
17
|
type: 'object',
|
|
17
18
|
properties: {
|
|
@@ -32,9 +33,9 @@ export default {
|
|
|
32
33
|
},
|
|
33
34
|
|
|
34
35
|
create(context) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const { allowedPrefixes } = { ...defaultOptions, ...context.options[0] },
|
|
37
|
+
allowedPrefixesArray = array.to(allowedPrefixes),
|
|
38
|
+
text = context.sourceCode.text
|
|
38
39
|
let scanned = false
|
|
39
40
|
function scan() {
|
|
40
41
|
if (scanned) return
|
|
@@ -45,7 +46,7 @@ export default {
|
|
|
45
46
|
tag = full.trimStart().toLowerCase().startsWith('<object') ? 'object' : 'embed',
|
|
46
47
|
attr = tag == 'object' ? 'data' : 'src',
|
|
47
48
|
val = html.pickAttrVal(attrs.match(tag == 'object' ? html.re.attr.data : html.re.attr.src))
|
|
48
|
-
if (val !== null && html.isRemote(val.trim(),
|
|
49
|
+
if (val !== null && html.isRemote(val.trim(), allowedPrefixesArray))
|
|
49
50
|
context.report({ loc: html.offsetToLoc(text, match.index), messageId: 'remoteObject',
|
|
50
51
|
data: { tag, attr, src: val.trim() }})
|
|
51
52
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import array from '../utils/array.js'
|
|
2
2
|
import regex from '../data/regex.js'
|
|
3
3
|
|
|
4
|
+
const defaultOptions = { allowedSchemes: ['blob', 'chrome-extension', 'data'], patterns: ['at-import', 'url'] }
|
|
5
|
+
|
|
4
6
|
function getPreludeURLnode(prelude) {
|
|
5
7
|
if (!prelude) return null
|
|
6
8
|
if (['Url', 'CSSUrl', 'String', 'CSSString'].includes(prelude.type))
|
|
@@ -28,10 +30,7 @@ export default {
|
|
|
28
30
|
category: 'Possible Errors',
|
|
29
31
|
css: true
|
|
30
32
|
},
|
|
31
|
-
defaultOptions: [
|
|
32
|
-
allowedSchemes: ['blob', 'chrome-extension', 'data'],
|
|
33
|
-
patterns: ['at-import', 'url']
|
|
34
|
-
}],
|
|
33
|
+
defaultOptions: [defaultOptions],
|
|
35
34
|
schema: [{
|
|
36
35
|
type: 'object',
|
|
37
36
|
properties: {
|
|
@@ -60,20 +59,20 @@ export default {
|
|
|
60
59
|
},
|
|
61
60
|
|
|
62
61
|
create(context) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
const { allowedSchemes, patterns } = { ...defaultOptions, ...context.options[0] },
|
|
63
|
+
allowedSchemeSet = new Set(array.to(allowedSchemes)),
|
|
64
|
+
patternSet = new Set(array.to(patterns)),
|
|
65
|
+
handledURLs = new WeakSet()
|
|
67
66
|
function handleAtrule(node) {
|
|
68
67
|
if (!node.prelude) return
|
|
69
|
-
if (node.name == 'import' &&
|
|
68
|
+
if (node.name == 'import' && patternSet.has('at-import')) {
|
|
70
69
|
const urlNode = getPreludeURLnode(node.prelude)
|
|
71
70
|
if (urlNode && isRemote(getURLstr(urlNode))) {
|
|
72
71
|
handledURLs.add(urlNode)
|
|
73
72
|
context.report({ node, messageId: 'remoteImport' })
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
|
-
if (node.name == 'namespace' &&
|
|
75
|
+
if (node.name == 'namespace' && patternSet.has('namespace')) {
|
|
77
76
|
const urlNode = getPreludeURLnode(node.prelude)
|
|
78
77
|
if (urlNode && isRemote(getURLstr(urlNode))) {
|
|
79
78
|
handledURLs.add(urlNode)
|
|
@@ -82,7 +81,7 @@ export default {
|
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
function handleURL(node) {
|
|
85
|
-
if (!
|
|
84
|
+
if (!patternSet.has('url')) return
|
|
86
85
|
if (handledURLs.has(node)) return
|
|
87
86
|
if (node.parent?.type == 'AtrulePrelude' || node.parent?.type == 'CSSAtRulePrelude') return
|
|
88
87
|
if (isRemote(getURLstr(node)))
|
|
@@ -95,7 +94,7 @@ export default {
|
|
|
95
94
|
if (match[0] == '//') return true
|
|
96
95
|
const scheme = (match[1] || '').toLowerCase()
|
|
97
96
|
if (['http', 'https', 'ftp'].includes(scheme)) return true
|
|
98
|
-
return !
|
|
97
|
+
return !allowedSchemeSet.has(scheme)
|
|
99
98
|
}
|
|
100
99
|
return { Atrule: handleAtrule, CSSAtRule: handleAtrule, Url: handleURL, CSSUrl: handleURL }
|
|
101
100
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import array from '../utils/array.js'
|
|
2
2
|
import node from '../utils/node.js'
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
4
|
+
const defaultOptions = { allowedSelectors: [] }, // oxlint-disable-next-line security/detect-unsafe-regex
|
|
5
|
+
re = { scrollbarPseudo: /^(?:-webkit-)?scrollbar(?:-[\w-]+)?$/i }
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
meta: {
|
|
@@ -13,7 +13,7 @@ export default {
|
|
|
13
13
|
category: 'Possible Errors',
|
|
14
14
|
css: true
|
|
15
15
|
},
|
|
16
|
-
defaultOptions: [
|
|
16
|
+
defaultOptions: [defaultOptions],
|
|
17
17
|
schema: [{
|
|
18
18
|
type: 'object',
|
|
19
19
|
properties: {
|
|
@@ -34,11 +34,11 @@ export default {
|
|
|
34
34
|
},
|
|
35
35
|
|
|
36
36
|
create(context) {
|
|
37
|
-
const
|
|
38
|
-
|
|
37
|
+
const { allowedSelectors } = { ...defaultOptions, ...context.options[0] },
|
|
38
|
+
allowedSelectorSet = new Set(array.to(allowedSelectors))
|
|
39
39
|
function checkSelector(selector) {
|
|
40
40
|
if (!selector) return
|
|
41
|
-
const text = getSelectorText(selector).trim() ; if (
|
|
41
|
+
const text = getSelectorText(selector).trim() ; if (allowedSelectorSet.has(text)) return
|
|
42
42
|
let hasScrollbarPseudo = false,
|
|
43
43
|
hasQualifier = false,
|
|
44
44
|
pastFirstCompound = false
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import array from '../utils/array.js'
|
|
1
2
|
import extension from '../utils/extension.js'
|
|
2
3
|
|
|
3
4
|
import permissions from '../data/permissions.js'
|
|
4
5
|
import regex from '../data/regex.js'
|
|
5
6
|
|
|
7
|
+
const defaultOptions = { allowedPermissions: [] }
|
|
8
|
+
|
|
6
9
|
export default {
|
|
7
10
|
meta: {
|
|
8
11
|
type: 'problem',
|
|
@@ -13,7 +16,7 @@ export default {
|
|
|
13
16
|
security: true,
|
|
14
17
|
manifest: true
|
|
15
18
|
},
|
|
16
|
-
defaultOptions: [
|
|
19
|
+
defaultOptions: [defaultOptions],
|
|
17
20
|
schema: [{
|
|
18
21
|
type: 'object',
|
|
19
22
|
properties: {
|
|
@@ -38,9 +41,8 @@ export default {
|
|
|
38
41
|
},
|
|
39
42
|
|
|
40
43
|
create(context) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
: opts.allowedPermissions ? [opts.allowedPermissions] : [])
|
|
44
|
+
const { allowedPermissions } = { ...defaultOptions, ...context.options[0] },
|
|
45
|
+
allowed = new Set(array.to(allowedPermissions))
|
|
44
46
|
function checkPermission(node, permission) {
|
|
45
47
|
if (typeof permission != 'string') return
|
|
46
48
|
if (allowed.has(permission) || !permissions.sensitive.has(permission)) return
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import extension from '../utils/extension.js'
|
|
2
2
|
import regex from '../data/regex.js'
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const defaultOptions = { allowedPatterns: [] },
|
|
5
|
+
re = { broadHost: /^(?:<all_urls>|\*:\/\/\*\/\*|\w+:\/\/\*\/\*)$/ },
|
|
5
6
|
hostKeys = new Set(['host_permissions', 'optional_host_permissions', 'optional_permissions', 'permissions'])
|
|
6
7
|
|
|
7
8
|
export default {
|
|
@@ -14,7 +15,7 @@ export default {
|
|
|
14
15
|
security: true,
|
|
15
16
|
manifest: true
|
|
16
17
|
},
|
|
17
|
-
defaultOptions: [
|
|
18
|
+
defaultOptions: [defaultOptions],
|
|
18
19
|
schema: [{
|
|
19
20
|
type: 'object',
|
|
20
21
|
properties: {
|
|
@@ -33,12 +34,10 @@ export default {
|
|
|
33
34
|
},
|
|
34
35
|
|
|
35
36
|
create(context) {
|
|
36
|
-
const allowedPatterns =
|
|
37
|
+
const { allowedPatterns } = { ...defaultOptions, ...context.options[0] },
|
|
38
|
+
allowedPatternSet = new Set(allowedPatterns)
|
|
37
39
|
function isBroad(pattern) {
|
|
38
|
-
return typeof pattern == 'string'
|
|
39
|
-
&& !allowedPatterns.has(pattern)
|
|
40
|
-
&& re.broadHost.test(pattern)
|
|
41
|
-
}
|
|
40
|
+
return typeof pattern == 'string' && !allowedPatternSet.has(pattern) && re.broadHost.test(pattern) }
|
|
42
41
|
if (regex.filename.manifest.test(extension.file.getName(context))) // lint it directly
|
|
43
42
|
return {
|
|
44
43
|
'*'(node) {
|
|
@@ -4,6 +4,8 @@ import i18nUtils from '../utils/i18n.js'
|
|
|
4
4
|
import i18nData from '../data/i18n.js'
|
|
5
5
|
import regex from '../data/regex.js'
|
|
6
6
|
|
|
7
|
+
const defaultOptions = { ignoreKeys: [], scanExtensions: i18nData.defaultScanExtensions }
|
|
8
|
+
|
|
7
9
|
export default {
|
|
8
10
|
meta: {
|
|
9
11
|
type: 'suggestion',
|
|
@@ -26,7 +28,7 @@ export default {
|
|
|
26
28
|
},
|
|
27
29
|
additionalProperties: false
|
|
28
30
|
}],
|
|
29
|
-
defaultOptions: [
|
|
31
|
+
defaultOptions: [defaultOptions],
|
|
30
32
|
messages: {
|
|
31
33
|
unusedKey: '`{{key}}` in `_locales/{{locale}}/messages.json` is never referenced.'
|
|
32
34
|
}
|
|
@@ -38,9 +40,8 @@ export default {
|
|
|
38
40
|
const manifest = extension.manifest.read(context) ; if (!manifest?.default_locale) return {}
|
|
39
41
|
const root = extension.findRoot(context) ; if (!root) return {}
|
|
40
42
|
const i18n = i18nUtils.readDefaultLocaleMessages(manifest, root) ; if (!i18n) return {}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
scanExtensions = opts.scanExtensions || i18nData.defaultScanExtensions
|
|
43
|
+
const { ignoreKeys, scanExtensions } = { ...defaultOptions, ...context.options[0] },
|
|
44
|
+
ignored = new Set(ignoreKeys)
|
|
44
45
|
function check() {
|
|
45
46
|
const { used, patterns } = i18nUtils.collectUsedKeys(root, scanExtensions)
|
|
46
47
|
for (const key of Object.keys(i18n.messages)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import extension from '../utils/extension.js'
|
|
2
2
|
import regex from '../data/regex.js'
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const defaultOptions = { maxNameLength: 12, always: false, checkRedundant: false },
|
|
5
5
|
re = { i18nPlaceholder: /^__MSG_.+__$/ }
|
|
6
6
|
|
|
7
7
|
function isI18n(value) { return typeof value == 'string' && re.i18nPlaceholder.test(value) }
|
|
@@ -32,9 +32,7 @@ export default {
|
|
|
32
32
|
},
|
|
33
33
|
additionalProperties: false
|
|
34
34
|
}],
|
|
35
|
-
defaultOptions: [
|
|
36
|
-
maxNameLength: defaults.maxNameLength, always: false, checkRedundant: false
|
|
37
|
-
}],
|
|
35
|
+
defaultOptions: [defaultOptions],
|
|
38
36
|
messages: {
|
|
39
37
|
missingShortName: '`name` exceeds {{max}} chars. Declare `short_name` to control truncation.',
|
|
40
38
|
shortNameTooLong: '`short_name` exceeds the recommended {{max}}-character limit (found {{length}}).',
|
|
@@ -44,10 +42,7 @@ export default {
|
|
|
44
42
|
|
|
45
43
|
create(context) {
|
|
46
44
|
if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
|
|
47
|
-
const
|
|
48
|
-
maxNameLength = opts.maxNameLength ?? defaults.maxNameLength,
|
|
49
|
-
always = opts.always ?? false,
|
|
50
|
-
checkRedundant = opts.checkRedundant ?? false
|
|
45
|
+
const { maxNameLength, always, checkRedundant } = { ...defaultOptions, ...context.options[0] }
|
|
51
46
|
let manifest
|
|
52
47
|
try { manifest = JSON.parse(context.sourceCode.text) } catch { return {} }
|
|
53
48
|
if (!manifest || typeof manifest != 'object') return {}
|