@markuplint/types 5.0.0-rc.2 → 5.0.0-rc.5
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/CHANGELOG.md +38 -0
- package/README.md +83 -75
- package/lib/check-multi-types.d.ts +2 -9
- package/lib/check-multi-types.js +2 -9
- package/lib/css-defs.d.ts +0 -8
- package/lib/css-defs.js +0 -8
- package/lib/css-overrides.d.ts +2 -5
- package/lib/css-overrides.js +2 -5
- package/lib/css-syntax.d.ts +0 -11
- package/lib/css-syntax.js +6 -18
- package/lib/css-tokenizers.d.ts +0 -6
- package/lib/css-tokenizers.js +0 -6
- package/lib/debug.d.ts +0 -3
- package/lib/debug.js +0 -3
- package/lib/defs.d.ts +0 -8
- package/lib/defs.js +308 -37
- package/lib/directive.d.ts +0 -14
- package/lib/directive.js +0 -14
- package/lib/enum.d.ts +0 -11
- package/lib/enum.js +0 -11
- package/lib/keyword-type.d.ts +0 -13
- package/lib/keyword-type.js +0 -13
- package/lib/list.d.ts +0 -13
- package/lib/list.js +0 -13
- package/lib/match-result.d.ts +0 -21
- package/lib/match-result.js +0 -21
- package/lib/number.d.ts +0 -12
- package/lib/number.js +0 -12
- package/lib/primitive/is-float.d.ts +3 -4
- package/lib/primitive/is-float.js +3 -4
- package/lib/primitive/is-int.d.ts +0 -5
- package/lib/primitive/is-int.js +0 -5
- package/lib/primitive/is-non-zero-uint.d.ts +0 -6
- package/lib/primitive/is-non-zero-uint.js +0 -6
- package/lib/primitive/is-quantity.d.ts +0 -8
- package/lib/primitive/is-quantity.js +0 -8
- package/lib/primitive/is-uint.d.ts +0 -6
- package/lib/primitive/is-uint.js +0 -6
- package/lib/primitive/range.d.ts +0 -8
- package/lib/primitive/range.js +0 -8
- package/lib/primitive/split-unit.d.ts +0 -6
- package/lib/primitive/split-unit.js +0 -6
- package/lib/rfc/is-bcp-47.d.ts +24 -1
- package/lib/rfc/is-bcp-47.js +88 -3
- package/lib/token/token-collection.d.ts +1 -111
- package/lib/token/token-collection.js +0 -108
- package/lib/token/token.d.ts +1 -73
- package/lib/token/token.js +0 -72
- package/lib/token/types.d.ts +0 -9
- package/lib/types.schema.d.ts +1 -1
- package/lib/w3c/check-content-security-policy.d.ts +18 -0
- package/lib/w3c/check-content-security-policy.js +363 -0
- package/lib/w3c/check-serialized-permissions-policy.d.ts +0 -2
- package/lib/w3c/check-serialized-permissions-policy.js +0 -2
- package/lib/whatwg/check-autocomplete.d.ts +26 -9
- package/lib/whatwg/check-autocomplete.js +42 -15
- package/lib/whatwg/check-datetime/date-string-with-optional-time.d.ts +11 -0
- package/lib/whatwg/check-datetime/date-string-with-optional-time.js +16 -0
- package/lib/whatwg/check-datetime/date-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/date-string.js +0 -2
- package/lib/whatwg/check-datetime/datetime-tokens.d.ts +0 -13
- package/lib/whatwg/check-datetime/datetime-tokens.js +0 -16
- package/lib/whatwg/check-datetime/duration-string.d.ts +0 -4
- package/lib/whatwg/check-datetime/duration-string.js +0 -4
- package/lib/whatwg/check-datetime/global-date-and-time-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/global-date-and-time-string.js +4 -4
- package/lib/whatwg/check-datetime/index.d.ts +0 -5
- package/lib/whatwg/check-datetime/index.js +0 -5
- package/lib/whatwg/check-datetime/local-date-and-time-string.d.ts +0 -4
- package/lib/whatwg/check-datetime/local-date-and-time-string.js +0 -4
- package/lib/whatwg/check-datetime/month-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/month-string.js +0 -2
- package/lib/whatwg/check-datetime/time-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/time-string.js +0 -2
- package/lib/whatwg/check-datetime/time-zone-offset-string.d.ts +0 -8
- package/lib/whatwg/check-datetime/time-zone-offset-string.js +0 -8
- package/lib/whatwg/check-datetime/week-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/week-string.js +0 -2
- package/lib/whatwg/check-datetime/year-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/year-string.js +0 -2
- package/lib/whatwg/check-datetime/yearless-date-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/yearless-date-string.js +0 -2
- package/lib/whatwg/check-email.js +0 -2
- package/lib/whatwg/check-http-equiv-content-type.d.ts +12 -0
- package/lib/whatwg/check-http-equiv-content-type.js +32 -0
- package/lib/whatwg/check-http-equiv-refresh.d.ts +21 -0
- package/lib/whatwg/check-http-equiv-refresh.js +106 -0
- package/lib/whatwg/check-media-query-list.d.ts +61 -0
- package/lib/whatwg/check-media-query-list.js +484 -0
- package/lib/whatwg/check-mime-type.d.ts +0 -4
- package/lib/whatwg/check-mime-type.js +55 -3
- package/lib/whatwg/check-simple-color.d.ts +2 -4
- package/lib/whatwg/check-simple-color.js +2 -4
- package/lib/whatwg/check-url.d.ts +15 -7
- package/lib/whatwg/check-url.js +333 -25
- package/lib/whatwg/is-abs-url.d.ts +0 -2
- package/lib/whatwg/is-abs-url.js +0 -2
- package/lib/whatwg/is-browser-context-name.d.ts +0 -2
- package/lib/whatwg/is-browser-context-name.js +0 -2
- package/lib/whatwg/is-itemprop-name.d.ts +0 -2
- package/lib/whatwg/is-itemprop-name.js +0 -2
- package/lib/whatwg/is-navigable-target-name.d.ts +0 -2
- package/lib/whatwg/is-navigable-target-name.js +0 -2
- package/package.json +6 -5
- package/types.schema.json +9 -0
- package/ARCHITECTURE.ja.md +0 -256
- package/ARCHITECTURE.md +0 -256
- package/SKILL.md +0 -100
- package/docs/check-pipeline.ja.md +0 -494
- package/docs/check-pipeline.md +0 -494
- package/docs/token-system.ja.md +0 -584
- package/docs/token-system.md +0 -584
- package/docs/type-system.ja.md +0 -623
- package/docs/type-system.md +0 -623
- package/docs/validators.ja.md +0 -536
- package/docs/validators.md +0 -536
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { log } from '../debug.js';
|
|
2
|
+
import { matched } from '../match-result.js';
|
|
3
|
+
import { Token } from '../token/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Fetch Directives — value grammar is a `serialized-source-list`.
|
|
6
|
+
*
|
|
7
|
+
* @see https://www.w3.org/TR/CSP3/#directives-fetch
|
|
8
|
+
*/
|
|
9
|
+
const FETCH_DIRECTIVES = new Set([
|
|
10
|
+
'child-src',
|
|
11
|
+
'connect-src',
|
|
12
|
+
'default-src',
|
|
13
|
+
'font-src',
|
|
14
|
+
'frame-src',
|
|
15
|
+
'img-src',
|
|
16
|
+
'manifest-src',
|
|
17
|
+
'media-src',
|
|
18
|
+
'object-src',
|
|
19
|
+
'script-src',
|
|
20
|
+
'script-src-attr',
|
|
21
|
+
'script-src-elem',
|
|
22
|
+
'style-src',
|
|
23
|
+
'style-src-attr',
|
|
24
|
+
'style-src-elem',
|
|
25
|
+
'worker-src',
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* Directives whose value is also a `serialized-source-list`, per
|
|
29
|
+
* CSP3 §6.7.2 ("This section defines the syntax common to all fetch
|
|
30
|
+
* directives, as well as `base-uri`, `form-action`, and `frame-ancestors`").
|
|
31
|
+
*
|
|
32
|
+
* @see https://www.w3.org/TR/CSP3/#framework-directive-source-list
|
|
33
|
+
*/
|
|
34
|
+
const SOURCE_LIST_DIRECTIVES = new Set([...FETCH_DIRECTIVES, 'base-uri', 'form-action', 'frame-ancestors']);
|
|
35
|
+
/**
|
|
36
|
+
* Directives that carry no value at all.
|
|
37
|
+
*
|
|
38
|
+
* @see https://www.w3.org/TR/upgrade-insecure-requests/#delivery
|
|
39
|
+
*/
|
|
40
|
+
const NO_VALUE_DIRECTIVES = new Set(['upgrade-insecure-requests']);
|
|
41
|
+
/**
|
|
42
|
+
* Directives recognized by name, whose value grammar this checker does not
|
|
43
|
+
* further validate (accepts anything, including empty): `report-uri` /
|
|
44
|
+
* `report-to` (URI-reference list / single token) and `webrtc` (`'allow'` /
|
|
45
|
+
* `'block'`), none of which are exercised by the nu-validator bench corpus.
|
|
46
|
+
*/
|
|
47
|
+
const UNVALIDATED_VALUE_DIRECTIVES = new Set(['report-uri', 'report-to', 'webrtc']);
|
|
48
|
+
/**
|
|
49
|
+
* The complete set of currently-specified CSP directive names: CSP3 §6
|
|
50
|
+
* itself (fetch directives, `base-uri`, `sandbox`, `form-action`,
|
|
51
|
+
* `frame-ancestors`, `report-uri`, `report-to`, `webrtc`), plus directives
|
|
52
|
+
* defined by companion specs that are nonetheless part of the CSP delivery
|
|
53
|
+
* mechanism and widely deployed: `require-trusted-types-for` / `trusted-types`
|
|
54
|
+
* (Trusted Types) and `upgrade-insecure-requests` (Upgrade Insecure Requests).
|
|
55
|
+
*
|
|
56
|
+
* @see https://www.w3.org/TR/CSP3/#directives
|
|
57
|
+
* @see https://www.w3.org/TR/trusted-types/#require-trusted-types-for-csp-directive
|
|
58
|
+
* @see https://www.w3.org/TR/trusted-types/#trusted-types-csp-directive
|
|
59
|
+
* @see https://www.w3.org/TR/upgrade-insecure-requests/#delivery
|
|
60
|
+
*/
|
|
61
|
+
const KNOWN_DIRECTIVES = new Set([
|
|
62
|
+
...SOURCE_LIST_DIRECTIVES,
|
|
63
|
+
...NO_VALUE_DIRECTIVES,
|
|
64
|
+
...UNVALIDATED_VALUE_DIRECTIVES,
|
|
65
|
+
'sandbox',
|
|
66
|
+
'require-trusted-types-for',
|
|
67
|
+
'trusted-types',
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* Mirrors the `sandbox` attribute's sandboxing-token enum in
|
|
71
|
+
* `packages/@markuplint/html-spec/src/spec.iframe.jsonc`. Keep the two in
|
|
72
|
+
* sync — there is no shared runtime source because `@markuplint/types` does
|
|
73
|
+
* not depend on `@markuplint/html-spec`.
|
|
74
|
+
*
|
|
75
|
+
* @see https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-sandbox
|
|
76
|
+
*/
|
|
77
|
+
const SANDBOX_TOKENS = new Set([
|
|
78
|
+
'allow-downloads',
|
|
79
|
+
'allow-forms',
|
|
80
|
+
'allow-modals',
|
|
81
|
+
'allow-orientation-lock',
|
|
82
|
+
'allow-pointer-lock',
|
|
83
|
+
'allow-popups',
|
|
84
|
+
'allow-popups-to-escape-sandbox',
|
|
85
|
+
'allow-presentation',
|
|
86
|
+
'allow-same-origin',
|
|
87
|
+
'allow-scripts',
|
|
88
|
+
'allow-top-navigation',
|
|
89
|
+
'allow-top-navigation-by-user-activation',
|
|
90
|
+
'allow-custom-protocols-navigation',
|
|
91
|
+
]);
|
|
92
|
+
/**
|
|
93
|
+
* `source-expression` keyword-source alternatives, excluding `'none'`
|
|
94
|
+
* (which the grammar restricts to being the sole item of the list).
|
|
95
|
+
*
|
|
96
|
+
* @see https://www.w3.org/TR/CSP3/#grammardef-source-expression
|
|
97
|
+
*/
|
|
98
|
+
const KEYWORD_SOURCES = new Set([
|
|
99
|
+
"'self'",
|
|
100
|
+
"'unsafe-inline'",
|
|
101
|
+
"'unsafe-eval'",
|
|
102
|
+
"'strict-dynamic'",
|
|
103
|
+
"'unsafe-hashes'",
|
|
104
|
+
"'report-sample'",
|
|
105
|
+
"'unsafe-allow-redirects'",
|
|
106
|
+
]);
|
|
107
|
+
const NONE_SOURCE = "'none'";
|
|
108
|
+
/**
|
|
109
|
+
* `scheme-part ":"`, e.g. `https:`, `data:`, `blob:`.
|
|
110
|
+
*/
|
|
111
|
+
const SCHEME_SOURCE = /^[A-Z][A-Z0-9+.-]*:$/i;
|
|
112
|
+
/**
|
|
113
|
+
* `[ scheme-part "://" ] host-part [ port-part ] [ path-part ]`.
|
|
114
|
+
* Non-ASCII authorities are already rejected by the ASCII-only guard in
|
|
115
|
+
* {@link checkContentSecurityPolicy} before this pattern is reached.
|
|
116
|
+
*/
|
|
117
|
+
const HOST_SOURCE = /^(?:[A-Z][A-Z0-9+.-]*:\/\/)?(?:\*|(?:\*\.)?[A-Z0-9-]+(?:\.[A-Z0-9-]+)*)(?::(?:\d+|\*))?(?:\/\S*)?$/i;
|
|
118
|
+
/**
|
|
119
|
+
* `"'nonce-" base64-value "'"`.
|
|
120
|
+
*/
|
|
121
|
+
const NONCE_SOURCE = /^'nonce-[\w+/-]+={0,2}'$/;
|
|
122
|
+
/**
|
|
123
|
+
* `"'" hash-algorithm "-" base64-value "'"`.
|
|
124
|
+
*/
|
|
125
|
+
const HASH_SOURCE = /^'sha(?:256|384|512)-[\w+/-]+={0,2}'$/;
|
|
126
|
+
/**
|
|
127
|
+
* @see https://www.w3.org/TR/CSP3/#grammardef-directive-name
|
|
128
|
+
*/
|
|
129
|
+
const DIRECTIVE_NAME = /^[A-Z0-9-]+$/i;
|
|
130
|
+
/**
|
|
131
|
+
* `trusted-types-policy-name` charset.
|
|
132
|
+
*
|
|
133
|
+
* @see https://www.w3.org/TR/trusted-types/#framework-directive-trusted-types
|
|
134
|
+
*/
|
|
135
|
+
const TRUSTED_TYPES_POLICY_NAME = /^[\w\-#=/@.%]+$/;
|
|
136
|
+
const TRUSTED_TYPES_KEYWORDS = new Set(["'allow-duplicates'", "'none'"]);
|
|
137
|
+
/**
|
|
138
|
+
* `directive-value = *( %x09 / %x20-2B / %x2D-3A / %x3C-7E )`, i.e. HTAB or
|
|
139
|
+
* printable ASCII. The full serialized policy (delimiters included) must be
|
|
140
|
+
* ASCII by construction, since `,`/`;` fall inside `%x20-7E` too.
|
|
141
|
+
*/
|
|
142
|
+
const ASCII_ONLY = /[^\t\u0020-\u007E]/;
|
|
143
|
+
/**
|
|
144
|
+
* Splits `token` on every occurrence of the literal `delimiter`, returning
|
|
145
|
+
* each piece as its own {@link Token} with an `offset` correct relative to
|
|
146
|
+
* `token.originalValue` — the position precision this checker needs to
|
|
147
|
+
* report the actual offending token (not the whole attribute value) is only
|
|
148
|
+
* available by tracking offsets through every split, the same approach
|
|
149
|
+
* `check-serialized-permissions-policy.ts` takes via `TokenCollection`.
|
|
150
|
+
*/
|
|
151
|
+
function splitToken(token, delimiter) {
|
|
152
|
+
const pieces = [];
|
|
153
|
+
let start = 0;
|
|
154
|
+
let idx = token.value.indexOf(delimiter, start);
|
|
155
|
+
while (idx !== -1) {
|
|
156
|
+
pieces.push(new Token(token.value.slice(start, idx), token.offset + start, token.originalValue));
|
|
157
|
+
start = idx + delimiter.length;
|
|
158
|
+
idx = token.value.indexOf(delimiter, start);
|
|
159
|
+
}
|
|
160
|
+
pieces.push(new Token(token.value.slice(start), token.offset + start, token.originalValue));
|
|
161
|
+
return pieces;
|
|
162
|
+
}
|
|
163
|
+
/** Same as {@link splitToken}, but on a whitespace-run pattern; empty pieces are dropped. */
|
|
164
|
+
function splitTokenByWhitespace(token) {
|
|
165
|
+
const pieces = [];
|
|
166
|
+
const re = /\s+/g;
|
|
167
|
+
let lastIndex = 0;
|
|
168
|
+
let match = re.exec(token.value);
|
|
169
|
+
while (match) {
|
|
170
|
+
pieces.push(new Token(token.value.slice(lastIndex, match.index), token.offset + lastIndex, token.originalValue));
|
|
171
|
+
lastIndex = match.index + match[0].length;
|
|
172
|
+
match = re.exec(token.value);
|
|
173
|
+
}
|
|
174
|
+
pieces.push(new Token(token.value.slice(lastIndex), token.offset + lastIndex, token.originalValue));
|
|
175
|
+
return pieces.filter(piece => piece.value.length > 0);
|
|
176
|
+
}
|
|
177
|
+
function trimToken(token) {
|
|
178
|
+
const leadingLength = token.value.length - token.value.trimStart().length;
|
|
179
|
+
return new Token(token.value.trim(), token.offset + leadingLength, token.originalValue);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Validates the `content` attribute value of
|
|
183
|
+
* `<meta http-equiv="content-security-policy">` against the Content
|
|
184
|
+
* Security Policy Level 3 `serialized-policy` grammar.
|
|
185
|
+
*
|
|
186
|
+
* Directive-value-specific grammars are only enforced for the directives
|
|
187
|
+
* exercised by the nu-validator bench corpus (fetch directives + `base-uri`
|
|
188
|
+
* / `form-action` / `frame-ancestors`, `sandbox`, `require-trusted-types-for`,
|
|
189
|
+
* `trusted-types`, `upgrade-insecure-requests`); `report-uri` / `report-to` /
|
|
190
|
+
* `webrtc` are recognized as registered directive names but their values are
|
|
191
|
+
* not further validated.
|
|
192
|
+
*
|
|
193
|
+
* @see https://www.w3.org/TR/CSP3/#framework-policy
|
|
194
|
+
* @see https://www.w3.org/TR/CSP3/#framework-directives
|
|
195
|
+
* @see https://www.w3.org/TR/CSP3/#meta-element
|
|
196
|
+
*/
|
|
197
|
+
export const checkContentSecurityPolicy = () => function checkContentSecurityPolicy(value) {
|
|
198
|
+
log('CHECK: meta http-equiv="content-security-policy" content');
|
|
199
|
+
// See ASCII_ONLY above. A non-ASCII authority (e.g. an IDN host written
|
|
200
|
+
// as Unicode rather than punycode) fails here first.
|
|
201
|
+
const nonAsciiMatch = ASCII_ONLY.exec(value);
|
|
202
|
+
if (nonAsciiMatch) {
|
|
203
|
+
const token = new Token(nonAsciiMatch[0], nonAsciiMatch.index, value);
|
|
204
|
+
return token.unmatched({
|
|
205
|
+
reason: 'syntax-error',
|
|
206
|
+
expects: [{ type: 'format', value: 'ASCII-only Content Security Policy' }],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The `<meta>` delivery algorithm strictly splits the attribute value
|
|
211
|
+
* on "," to obtain a list of independently-parsed policies, mirroring
|
|
212
|
+
* how multiple `Content-Security-Policy` HTTP header instances combine.
|
|
213
|
+
*
|
|
214
|
+
* @see https://www.w3.org/TR/CSP3/#meta-element
|
|
215
|
+
*/
|
|
216
|
+
const root = new Token(value, 0, value);
|
|
217
|
+
for (const policyToken of splitToken(root, ',')) {
|
|
218
|
+
const result = checkSerializedPolicy(policyToken);
|
|
219
|
+
if (!result.matched)
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
return matched();
|
|
223
|
+
};
|
|
224
|
+
function checkSerializedPolicy(policyToken) {
|
|
225
|
+
for (const rawDirectiveToken of splitToken(policyToken, ';')) {
|
|
226
|
+
const directiveToken = trimToken(rawDirectiveToken);
|
|
227
|
+
// OWS-only segments are permitted: a trailing ";", an empty policy
|
|
228
|
+
// (`content=""`), and the separator between adjacent directives.
|
|
229
|
+
if (!directiveToken.value)
|
|
230
|
+
continue;
|
|
231
|
+
const spaceIndex = directiveToken.value.search(/\s/);
|
|
232
|
+
const nameToken = spaceIndex === -1
|
|
233
|
+
? directiveToken
|
|
234
|
+
: new Token(directiveToken.value.slice(0, spaceIndex), directiveToken.offset, directiveToken.originalValue);
|
|
235
|
+
const name = nameToken.value.toLowerCase();
|
|
236
|
+
const directiveValueToken = spaceIndex === -1
|
|
237
|
+
? new Token('', directiveToken.offset + directiveToken.value.length, directiveToken.originalValue)
|
|
238
|
+
: trimToken(new Token(directiveToken.value.slice(spaceIndex + 1), directiveToken.offset + spaceIndex + 1, directiveToken.originalValue));
|
|
239
|
+
if (!DIRECTIVE_NAME.test(nameToken.value)) {
|
|
240
|
+
return nameToken.unmatched({
|
|
241
|
+
reason: 'unexpected-token',
|
|
242
|
+
expects: [{ type: 'format', value: 'directive-name' }],
|
|
243
|
+
partName: 'directive-name',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (!KNOWN_DIRECTIVES.has(name)) {
|
|
247
|
+
return nameToken.unmatched({
|
|
248
|
+
reason: 'doesnt-exist-in-enum',
|
|
249
|
+
expects: [{ type: 'common', value: 'registered CSP directive name' }],
|
|
250
|
+
partName: 'directive-name',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const result = checkDirectiveValue(name, directiveValueToken);
|
|
254
|
+
if (!result.matched)
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
return matched();
|
|
258
|
+
}
|
|
259
|
+
function checkDirectiveValue(name, valueToken) {
|
|
260
|
+
if (SOURCE_LIST_DIRECTIVES.has(name))
|
|
261
|
+
return checkSourceList(valueToken);
|
|
262
|
+
if (name === 'sandbox')
|
|
263
|
+
return checkSandbox(valueToken);
|
|
264
|
+
if (name === 'require-trusted-types-for')
|
|
265
|
+
return checkRequireTrustedTypesFor(valueToken);
|
|
266
|
+
if (name === 'trusted-types')
|
|
267
|
+
return checkTrustedTypes(valueToken);
|
|
268
|
+
if (NO_VALUE_DIRECTIVES.has(name) && valueToken.value) {
|
|
269
|
+
return valueToken.unmatched({
|
|
270
|
+
reason: 'extra-token',
|
|
271
|
+
extra: { type: 'common', value: 'directive-value' },
|
|
272
|
+
partName: name,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// UNVALIDATED_VALUE_DIRECTIVES (report-uri / report-to / webrtc): accept anything.
|
|
276
|
+
return matched();
|
|
277
|
+
}
|
|
278
|
+
function checkSourceList(valueToken) {
|
|
279
|
+
if (!valueToken.value)
|
|
280
|
+
return matched();
|
|
281
|
+
const tokens = splitTokenByWhitespace(valueToken);
|
|
282
|
+
// `serialized-source-list = (source-expression *(RWS source-expression)) / "'none'"`
|
|
283
|
+
// — `'none'` is only valid as the sole item of the list.
|
|
284
|
+
if (tokens.length > 1) {
|
|
285
|
+
const noneToken = tokens.find(token => token.value.toLowerCase() === NONE_SOURCE);
|
|
286
|
+
if (noneToken) {
|
|
287
|
+
return noneToken.unmatched({
|
|
288
|
+
reason: 'illegal-combination',
|
|
289
|
+
expects: [{ type: 'format', value: 'source-expression' }],
|
|
290
|
+
partName: 'source-list',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const token of tokens) {
|
|
295
|
+
const lower = token.value.toLowerCase();
|
|
296
|
+
if (lower === NONE_SOURCE ||
|
|
297
|
+
token.value === '*' ||
|
|
298
|
+
KEYWORD_SOURCES.has(lower) ||
|
|
299
|
+
NONCE_SOURCE.test(token.value) ||
|
|
300
|
+
HASH_SOURCE.test(token.value) ||
|
|
301
|
+
SCHEME_SOURCE.test(token.value) ||
|
|
302
|
+
HOST_SOURCE.test(token.value)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
return token.unmatched({
|
|
306
|
+
reason: 'unexpected-token',
|
|
307
|
+
expects: [{ type: 'format', value: 'source-expression' }],
|
|
308
|
+
partName: 'source-expression',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return matched();
|
|
312
|
+
}
|
|
313
|
+
function checkSandbox(valueToken) {
|
|
314
|
+
if (!valueToken.value)
|
|
315
|
+
return matched();
|
|
316
|
+
for (const token of splitTokenByWhitespace(valueToken)) {
|
|
317
|
+
if (!SANDBOX_TOKENS.has(token.value.toLowerCase())) {
|
|
318
|
+
return token.unmatched({
|
|
319
|
+
reason: 'doesnt-exist-in-enum',
|
|
320
|
+
expects: [{ type: 'common', value: 'sandboxing-token' }],
|
|
321
|
+
partName: 'sandbox',
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return matched();
|
|
326
|
+
}
|
|
327
|
+
function checkRequireTrustedTypesFor(valueToken) {
|
|
328
|
+
const tokens = splitTokenByWhitespace(valueToken);
|
|
329
|
+
if (tokens.length === 0) {
|
|
330
|
+
return valueToken.unmatched({
|
|
331
|
+
reason: 'missing-token',
|
|
332
|
+
expects: [{ type: 'const', value: "'script'" }],
|
|
333
|
+
partName: 'require-trusted-types-for',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
for (const token of tokens) {
|
|
337
|
+
if (token.value.toLowerCase() !== "'script'") {
|
|
338
|
+
return token.unmatched({
|
|
339
|
+
reason: 'doesnt-exist-in-enum',
|
|
340
|
+
expects: [{ type: 'const', value: "'script'" }],
|
|
341
|
+
partName: 'require-trusted-types-for',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return matched();
|
|
346
|
+
}
|
|
347
|
+
function checkTrustedTypes(valueToken) {
|
|
348
|
+
if (!valueToken.value)
|
|
349
|
+
return matched();
|
|
350
|
+
for (const token of splitTokenByWhitespace(valueToken)) {
|
|
351
|
+
if (token.value === '*' ||
|
|
352
|
+
TRUSTED_TYPES_KEYWORDS.has(token.value.toLowerCase()) ||
|
|
353
|
+
TRUSTED_TYPES_POLICY_NAME.test(token.value)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
return token.unmatched({
|
|
357
|
+
reason: 'unexpected-token',
|
|
358
|
+
expects: [{ type: 'format', value: 'trusted-types-value' }],
|
|
359
|
+
partName: 'trusted-types',
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return matched();
|
|
363
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { CustomSyntaxChecker } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Validates a serialized permissions policy string according to the W3C specification.
|
|
4
|
-
*
|
|
5
3
|
* @see https://w3c.github.io/webappsec-permissions-policy/#serialized-permissions-policy
|
|
6
4
|
*
|
|
7
5
|
* > Policy Directives in HTML attributes are represented as their
|
|
@@ -2,8 +2,6 @@ import { log } from '../debug.js';
|
|
|
2
2
|
import { matched, unmatched } from '../match-result.js';
|
|
3
3
|
import { TokenCollection } from '../token/index.js';
|
|
4
4
|
/**
|
|
5
|
-
* Validates a serialized permissions policy string according to the W3C specification.
|
|
6
|
-
*
|
|
7
5
|
* @see https://w3c.github.io/webappsec-permissions-policy/#serialized-permissions-policy
|
|
8
6
|
*
|
|
9
7
|
* > Policy Directives in HTML attributes are represented as their
|
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
import type { CustomSyntaxChecker } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Element/state-specific tightening applied on top of the shared
|
|
4
|
+
* autocomplete grammar.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* - `noWebauthn`: reject the `webauthn` token. Applies to elements
|
|
7
|
+
* where webauthn is not valid per HTML LS
|
|
8
|
+
* §attr-fe-autocomplete-webauthn ("webauthn is only valid for input
|
|
9
|
+
* and textarea elements"). Wired for `<select>`.
|
|
10
|
+
* - `anchorMantle`: reject the `on` / `off` keywords per HTML LS
|
|
11
|
+
* §autofill-anchor-mantle: "When wearing the autofill anchor
|
|
12
|
+
* mantle, the autocomplete attribute [...] must have a value that is
|
|
13
|
+
* an ordered set of space-separated tokens consisting of just
|
|
14
|
+
* autofill detail tokens (i.e. the 'on' and 'off' keywords are not
|
|
15
|
+
* allowed)." The anchor mantle applies to `<input type=hidden>`
|
|
16
|
+
* only; every other autocomplete-carrying control wears the
|
|
17
|
+
* expectation mantle where on/off are valid.
|
|
18
|
+
*
|
|
19
|
+
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-webauthn
|
|
20
|
+
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-anchor-mantle
|
|
21
|
+
*/
|
|
22
|
+
export type CheckAutoCompleteOptions = {
|
|
23
|
+
readonly noWebauthn?: boolean;
|
|
24
|
+
readonly anchorMantle?: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Parses backward (right-to-left) to match the spec algorithm, which is
|
|
28
|
+
* anchored on the trailing field name.
|
|
12
29
|
*
|
|
13
30
|
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete
|
|
14
31
|
*/
|
|
15
|
-
export declare const checkAutoComplete: CustomSyntaxChecker
|
|
32
|
+
export declare const checkAutoComplete: CustomSyntaxChecker<CheckAutoCompleteOptions>;
|
|
@@ -89,9 +89,6 @@ const URL_NAMED_GROUP = 'https://html.spec.whatwg.org/multipage/form-control-inf
|
|
|
89
89
|
const URL_PART_OF_ADDRESS = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-shipping';
|
|
90
90
|
const URL_AUTOFILL_FIELD = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field';
|
|
91
91
|
/**
|
|
92
|
-
* Determines the field category and maximum allowed token count
|
|
93
|
-
* based on the last meaningful token (the field name).
|
|
94
|
-
*
|
|
95
92
|
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field
|
|
96
93
|
*/
|
|
97
94
|
function determineFieldCategory(value) {
|
|
@@ -108,19 +105,12 @@ function determineFieldCategory(value) {
|
|
|
108
105
|
return null;
|
|
109
106
|
}
|
|
110
107
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* Uses backward parsing (right-to-left) to match the spec algorithm:
|
|
114
|
-
* 1. Determine field name from the last token
|
|
115
|
-
* 2. Handle `webauthn` credential token and category re-determination
|
|
116
|
-
* 3. Validate optional contacting token (home/work/mobile/fax/pager)
|
|
117
|
-
* 4. Validate optional shipping/billing token
|
|
118
|
-
* 5. Validate optional section-* named group
|
|
119
|
-
* 6. Check maximum token count per category
|
|
108
|
+
* Parses backward (right-to-left) to match the spec algorithm, which is
|
|
109
|
+
* anchored on the trailing field name.
|
|
120
110
|
*
|
|
121
111
|
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete
|
|
122
112
|
*/
|
|
123
|
-
export const checkAutoComplete = () => value => {
|
|
113
|
+
export const checkAutoComplete = (options = {}) => value => {
|
|
124
114
|
const tokens = new TokenCollection(value, {
|
|
125
115
|
disallowToSurroundBySpaces: false,
|
|
126
116
|
allowEmpty: false,
|
|
@@ -146,6 +136,23 @@ export const checkAutoComplete = () => value => {
|
|
|
146
136
|
// Never — TokenCollection.check would catch empty
|
|
147
137
|
throw new Error('TokenCollection is empty');
|
|
148
138
|
}
|
|
139
|
+
// Anchor-mantle gate: HTML LS §autofill-anchor-mantle forbids the
|
|
140
|
+
// on/off keywords anywhere in the value ("consisting of just
|
|
141
|
+
// autofill detail tokens"). Scan every token so the diagnostic
|
|
142
|
+
// cites the anchor-mantle spec even when on/off is not the first
|
|
143
|
+
// token (e.g. `name on`, `off name`); the "unknown field name"
|
|
144
|
+
// fallback further down would otherwise mask the true reason.
|
|
145
|
+
if (options.anchorMantle) {
|
|
146
|
+
const onOffToken = identTokens.find(t => t.matches(['on', 'off'], true));
|
|
147
|
+
if (onOffToken) {
|
|
148
|
+
acLog('[Unmatched ("%s")] on/off keyword rejected in autofill anchor mantle', value);
|
|
149
|
+
return onOffToken.unmatched({
|
|
150
|
+
reason: 'unexpected-token',
|
|
151
|
+
expects: [{ type: 'common', value: 'autofill field name' }],
|
|
152
|
+
ref: 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-anchor-mantle',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
149
156
|
const firstToken = identTokens[0];
|
|
150
157
|
// Check for "on" / "off"
|
|
151
158
|
if (firstToken.matches(['on', 'off'], true)) {
|
|
@@ -221,6 +228,19 @@ export const checkAutoComplete = () => value => {
|
|
|
221
228
|
index--;
|
|
222
229
|
// Step 2: Handle webauthn (Credential category re-determination)
|
|
223
230
|
if (category === 'Credential') {
|
|
231
|
+
// Per HTML LS §attr-fe-autocomplete-webauthn, webauthn "is only valid
|
|
232
|
+
// for input and textarea elements." Elements that pass `noWebauthn`
|
|
233
|
+
// (button / fieldset / object / output / select) reject the token
|
|
234
|
+
// with a spec-cited unexpected-token result so the diagnostic
|
|
235
|
+
// distinguishes it from a generic unknown field name.
|
|
236
|
+
if (options.noWebauthn) {
|
|
237
|
+
acLog('[Unmatched ("%s")] webauthn is not valid on this element', value);
|
|
238
|
+
return lastToken.unmatched({
|
|
239
|
+
reason: 'unexpected-token',
|
|
240
|
+
expects: [{ type: 'common', value: 'autofill field name' }],
|
|
241
|
+
ref: 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-webauthn',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
224
244
|
// webauthn token consumed; if there are more tokens, re-determine category
|
|
225
245
|
if (index >= 0) {
|
|
226
246
|
const preWebauthnToken = identTokens[index];
|
|
@@ -253,8 +273,15 @@ export const checkAutoComplete = () => value => {
|
|
|
253
273
|
}
|
|
254
274
|
}
|
|
255
275
|
else {
|
|
256
|
-
//
|
|
257
|
-
|
|
276
|
+
// Spec: "The webauthn token must not be the only token in the
|
|
277
|
+
// autocomplete attribute's value."
|
|
278
|
+
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-webauthn
|
|
279
|
+
acLog('[Unmatched ("%s")] Standalone webauthn rejected', value);
|
|
280
|
+
return lastToken.unmatched({
|
|
281
|
+
reason: 'unexpected-token',
|
|
282
|
+
expects: [{ type: 'common', value: 'autofill field name' }],
|
|
283
|
+
ref: URL_AUTOFILL_FIELD,
|
|
284
|
+
});
|
|
258
285
|
}
|
|
259
286
|
}
|
|
260
287
|
// No more tokens to validate — only the field name was present
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CustomSyntaxChecker } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Used by attributes whose spec defines the value as "a valid date string with
|
|
4
|
+
* optional time" (for example `<del>`/`<ins>` `datetime`). The accepted
|
|
5
|
+
* production is strictly the union of those two formats — month-only, year-only,
|
|
6
|
+
* week, yearless date, time-only, local datetime, and duration strings are all
|
|
7
|
+
* rejected.
|
|
8
|
+
*
|
|
9
|
+
* @see https://html.spec.whatwg.org/multipage/edits.html#attr-mod-datetime
|
|
10
|
+
*/
|
|
11
|
+
export declare const checkDateStringWithOptionalTime: CustomSyntaxChecker;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { checkMultiTypes } from '../../check-multi-types.js';
|
|
2
|
+
import { checkDateString } from './date-string.js';
|
|
3
|
+
import { checkGlobalDateAndTimeString } from './global-date-and-time-string.js';
|
|
4
|
+
const checks = [checkDateString(), checkGlobalDateAndTimeString()];
|
|
5
|
+
/**
|
|
6
|
+
* Used by attributes whose spec defines the value as "a valid date string with
|
|
7
|
+
* optional time" (for example `<del>`/`<ins>` `datetime`). The accepted
|
|
8
|
+
* production is strictly the union of those two formats — month-only, year-only,
|
|
9
|
+
* week, yearless date, time-only, local datetime, and duration strings are all
|
|
10
|
+
* rejected.
|
|
11
|
+
*
|
|
12
|
+
* @see https://html.spec.whatwg.org/multipage/edits.html#attr-mod-datetime
|
|
13
|
+
*/
|
|
14
|
+
export const checkDateStringWithOptionalTime = () => value => {
|
|
15
|
+
return checkMultiTypes(value, checks);
|
|
16
|
+
};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { CustomSyntaxChecker } from '../../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Validates a date string in the format `YYYY-MM-DD`.
|
|
4
|
-
*
|
|
5
3
|
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#dates
|
|
6
4
|
*/
|
|
7
5
|
export declare const checkDateString: CustomSyntaxChecker;
|
|
@@ -2,8 +2,6 @@ import { log } from '../../debug.js';
|
|
|
2
2
|
import { TokenCollection } from '../../token/index.js';
|
|
3
3
|
import { datetimeTokenCheck } from './datetime-tokens.js';
|
|
4
4
|
/**
|
|
5
|
-
* Validates a date string in the format `YYYY-MM-DD`.
|
|
6
|
-
*
|
|
7
5
|
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#dates
|
|
8
6
|
*/
|
|
9
7
|
export const checkDateString = () => function checkDateString(value) {
|
|
@@ -1,16 +1,3 @@
|
|
|
1
1
|
import type { TokenEachCheck } from '../../token/token-collection.js';
|
|
2
|
-
/**
|
|
3
|
-
* Collection of token check functions for datetime component validation.
|
|
4
|
-
*
|
|
5
|
-
* Each property is a {@link TokenEachCheck} function that validates a specific
|
|
6
|
-
* datetime component (year, month, date, hour, minute, second, etc.)
|
|
7
|
-
* according to the WHATWG specification.
|
|
8
|
-
*/
|
|
9
2
|
export declare const datetimeTokenCheck: Record<'year' | 'month' | 'date' | 'hour' | 'minute' | 'second' | 'secondFractionalPart' | 'week' | 'hyphen' | 'colon' | 'extra' | 'colonOrEnd' | 'decimalPointOrEnd' | 'localDateTimeSeparator' | 'normalizedlocalDateTimeSeparator' | 'plusOrMinusSign' | 'weekSign', TokenEachCheck> & Record<'_year' | '_month', number | null>;
|
|
10
|
-
/**
|
|
11
|
-
* Calculates the maximum ISO week number for a given year.
|
|
12
|
-
*
|
|
13
|
-
* @param year - The year to calculate for
|
|
14
|
-
* @returns The maximum week number (52 or 53) for the year
|
|
15
|
-
*/
|
|
16
3
|
export declare function getMaxWeekNum(year: number): number;
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { log } from '../../debug.js';
|
|
2
2
|
import { matched, unmatched } from '../../match-result.js';
|
|
3
|
-
/**
|
|
4
|
-
* Collection of token check functions for datetime component validation.
|
|
5
|
-
*
|
|
6
|
-
* Each property is a {@link TokenEachCheck} function that validates a specific
|
|
7
|
-
* datetime component (year, month, date, hour, minute, second, etc.)
|
|
8
|
-
* according to the WHATWG specification.
|
|
9
|
-
*/
|
|
10
3
|
export const datetimeTokenCheck = {
|
|
11
4
|
/**
|
|
12
5
|
* Temporary year state
|
|
@@ -457,9 +450,6 @@ export const datetimeTokenCheck = {
|
|
|
457
450
|
});
|
|
458
451
|
}
|
|
459
452
|
},
|
|
460
|
-
/**
|
|
461
|
-
* Extra token
|
|
462
|
-
*/
|
|
463
453
|
extra(extra) {
|
|
464
454
|
log('Parsing Datetime EXTRA STRING: "%s"', extra?.value);
|
|
465
455
|
if (extra && extra.value) {
|
|
@@ -496,12 +486,6 @@ const daysOfMonth = [
|
|
|
496
486
|
// 12
|
|
497
487
|
31,
|
|
498
488
|
];
|
|
499
|
-
/**
|
|
500
|
-
* Calculates the maximum ISO week number for a given year.
|
|
501
|
-
*
|
|
502
|
-
* @param year - The year to calculate for
|
|
503
|
-
* @returns The maximum week number (52 or 53) for the year
|
|
504
|
-
*/
|
|
505
489
|
export function getMaxWeekNum(year) {
|
|
506
490
|
let date = 31;
|
|
507
491
|
while (date > 0) {
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import type { CustomSyntaxChecker } from '../../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Validates a duration string in ISO 8601-like format (e.g., `PT1H30M`).
|
|
4
|
-
*
|
|
5
3
|
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#durations
|
|
6
4
|
*/
|
|
7
5
|
export declare const checkDurationISO8601LikeString: CustomSyntaxChecker;
|
|
8
6
|
/**
|
|
9
|
-
* Validates a duration string in component list format (e.g., `1h 30m 5s`).
|
|
10
|
-
*
|
|
11
7
|
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#durations
|
|
12
8
|
*/
|
|
13
9
|
export declare const checkDurationComponentListString: CustomSyntaxChecker;
|
|
@@ -3,8 +3,6 @@ import { matched, unmatched } from '../../match-result.js';
|
|
|
3
3
|
import { TokenCollection } from '../../token/index.js';
|
|
4
4
|
import { datetimeTokenCheck } from './datetime-tokens.js';
|
|
5
5
|
/**
|
|
6
|
-
* Validates a duration string in ISO 8601-like format (e.g., `PT1H30M`).
|
|
7
|
-
*
|
|
8
6
|
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#durations
|
|
9
7
|
*/
|
|
10
8
|
export const checkDurationISO8601LikeString = () => function checkDurationISO8601LikeString(value) {
|
|
@@ -204,8 +202,6 @@ export const checkDurationISO8601LikeString = () => function checkDurationISO860
|
|
|
204
202
|
return res;
|
|
205
203
|
};
|
|
206
204
|
/**
|
|
207
|
-
* Validates a duration string in component list format (e.g., `1h 30m 5s`).
|
|
208
|
-
*
|
|
209
205
|
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#durations
|
|
210
206
|
*/
|
|
211
207
|
export const checkDurationComponentListString = () => function checkDurationComponentListString(value) {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { CustomSyntaxChecker } from '../../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Validates a global date and time string (date + time + time-zone offset).
|
|
4
|
-
*
|
|
5
3
|
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#global-dates-and-times
|
|
6
4
|
*/
|
|
7
5
|
export declare const checkGlobalDateAndTimeString: CustomSyntaxChecker;
|