-
Notifications
You must be signed in to change notification settings - Fork 667
/
hint.ts
170 lines (129 loc) · 5.96 KB
/
hint.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/**
* @fileoverview Check the usage of the `Content-Type` HTTP response
* header.
*/
/*
* ------------------------------------------------------------------------------
* Requirements
* ------------------------------------------------------------------------------
*/
import { MediaType, parse } from 'content-type';
import { Category } from 'hint/dist/src/lib/enums/category';
import { debug as d } from 'hint/dist/src/lib/utils/debug';
import { IAsyncHTMLElement, Response, IHint, FetchEnd, HintMetadata } from 'hint/dist/src/lib/types';
import getHeaderValueNormalized from 'hint/dist/src/lib/utils/network/normalized-header-value';
import isDataURI from 'hint/dist/src/lib/utils/network/is-data-uri';
import normalizeString from 'hint/dist/src/lib/utils/misc/normalize-string';
import { isTextMediaType } from 'hint/dist/src/lib/utils/content-type';
import { HintContext } from 'hint/dist/src/lib/hint-context';
import { HintScope } from 'hint/dist/src/lib/enums/hintscope';
const debug = d(__filename);
/*
* ------------------------------------------------------------------------------
* Public
* ------------------------------------------------------------------------------
*/
export default class ContentTypeHint implements IHint {
public static readonly meta: HintMetadata = {
docs: {
category: Category.interoperability,
description: 'Require `Content-Type` header with appropriate value'
},
id: 'content-type',
schema: [{
items: { type: 'string' },
type: ['object', 'null'],
uniqueItems: true
}],
scope: HintScope.site
}
public constructor(context: HintContext) {
let userDefinedMediaTypes: { [regex: string]: string };
const loadHintConfigs = () => {
userDefinedMediaTypes = context.hintOptions || {};
};
const getLastRegexThatMatches = (resource: string): string | undefined => {
const results = (Object.entries(userDefinedMediaTypes).filter(([regex]) => {
const re = new RegExp(regex, 'i');
return re.test(resource);
}))
.pop();
return results && results[1];
};
const validate = async (fetchEnd: FetchEnd) => {
const { element, resource, response }: { element: IAsyncHTMLElement | null, resource: string, response: Response } = fetchEnd;
if (response.statusCode !== 200) {
debug(`Check does not apply to status code !== 200`);
return;
}
// This check does not make sense for data URIs.
if (isDataURI(resource)) {
debug(`Check does not apply for data URIs`);
return;
}
const contentTypeHeaderValue: string | null = getHeaderValueNormalized(response.headers, 'content-type');
// Check if the `Content-Type` header was sent.
if (contentTypeHeaderValue === null) {
await context.report(resource, `Response should include 'content-type' header.`, { element });
return;
}
/*
* If the current resource matches any of the regexes
* defined by the user, use that value to validate.
*/
const userDefinedMediaType: string | undefined = getLastRegexThatMatches(resource);
if (userDefinedMediaType) {
if (normalizeString(userDefinedMediaType) !== contentTypeHeaderValue) {
await context.report(resource, `'content-type' header value should be '${userDefinedMediaType}'.`, { element });
}
return;
}
// Check if the `Content-Type` value is valid.
let contentType: MediaType;
try {
if (contentTypeHeaderValue === '') {
throw new TypeError('invalid media type');
}
contentType = parse(contentTypeHeaderValue);
} catch (e) {
await context.report(resource, `'content-type' header value should be valid (${e.message}).`, { element });
return;
}
const originalCharset: string | null = normalizeString(contentType.parameters ? contentType.parameters.charset : '');
const originalMediaType: string = contentType.type;
/*
* Determined values
*
* Notes:
*
* * The connectors already did all the heavy lifting here.
* * For the charset, recommend `utf-8` for all text based
* bases documents.
*/
const mediaType: string = response.mediaType;
const charset: string = isTextMediaType(mediaType) ? 'utf-8' : response.charset;
/*
* Check if the determined values differ
* from the ones from the `Content-Type` header.
*/
// * media type
if (mediaType && (mediaType !== originalMediaType)) {
await context.report(resource, `'content-type' header media type value should be '${mediaType}', not '${originalMediaType}'.`, { element });
}
// * charset value
if (charset) {
if (!originalCharset || (charset !== originalCharset)) {
await context.report(resource, `'content-type' header charset value should be '${charset}'${originalCharset ? `, not '${originalCharset}'` : ''}.`, { element });
}
} else if (originalCharset &&
![
'text/html',
'application/xhtml+xml'
].includes(originalMediaType)) {
await context.report(resource, `'content-type' header value should not contain 'charset=${originalCharset}'.`, { element });
}
};
loadHintConfigs();
context.on('fetch::end::*', validate);
}
}