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.
@@ -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 (when omitted, only year and month are checked)'
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 (expected format: {{example}}).',
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)