3 const common
= require('./common');
4 const Enum
= require('./enum');
7 // A weight value smaller than the allowed resolution, for minute wildcard de-preferencing.
8 const WeightIota
= 0.0001;
10 class ContentNegotiation
{
13 * Convert accept clause string to object.
14 * Adjust weight based on wildcards, to prefer literal matches.
15 * @param {string} clause
17 static _unpackAcceptClause(clause
) {
18 let params
= clause
.split(';');
19 const type
= params
.shift().trim();
22 params
= params
.reduce((acc
, param
) => {
23 const [p
, v
] = common
.splitFirst(param
, '=').map((x
) => x
&& x
.trim());
27 // Enforce max precision from spec, so that we can...
28 weight
= Number(weight
.toFixed(3));
29 // De-tune the quality slightly for wildcards.
30 const blur
= (type
.split('*').length
- 1) * WeightIota
;
35 // eslint-disable-next-line security/detect-object-injection
41 return { type
, weight
, params
};
46 * Split an accept field into clauses, return list of clauses sorted by heaviest weights first.
47 * @param {string} acceptHeader
49 static _acceptClauses(acceptHeader
) {
50 const clauses
= (acceptHeader
||'').split(',').map((clause
) => ContentNegotiation
._unpackAcceptClause(clause
)).filter((clause
) => clause
);
51 return clauses
.sort((a
, b
) => b
.weight
- a
.weight
);
55 * Check if an Accept-able Content-Type matches a fixed Content-Type.
56 * (Allows for '*' fields in Accept-able type.)
57 * @param {string} pattern
58 * @param {string} type
60 static _matchType(acceptableType
, fixedType
) {
61 acceptableType
= common
.splitFirst(acceptableType
, '/', '*');
62 fixedType
= common
.splitFirst(fixedType
, '/', '*');
63 for (let i
= 0; i
< acceptableType
.length
; i
++) {
64 // eslint-disable-next-line security/detect-object-injection
65 const v
= acceptableType
[i
];
66 // eslint-disable-next-line security/detect-object-injection
67 const f
= fixedType
[i
];
68 if (v
!== f
&& v
!== '*') {
76 * Return the best match between available and acceptable types.
77 * @param {string[]} acceptableTypes
78 * @param {string} acceptHeader
80 static accept(acceptableTypes
, acceptHeader
) {
81 const validTypesQuality
= {};
83 return acceptableTypes
[0];
85 // For each type offered in the header, from heaviest to lightest...
86 ContentNegotiation
._acceptClauses(acceptHeader
).forEach((a
) => {
87 // Consider each supported type...
88 acceptableTypes
.forEach((t
) => {
89 // Remember the heaviest weighting if it matches.
90 if (ContentNegotiation
._matchType(a
.type
, t
)
91 // eslint-disable-next-line security/detect-object-injection
92 && (!(t
in validTypesQuality
) || validTypesQuality
[t
] < a
.weight
)) {
93 // eslint-disable-next-line security/detect-object-injection
94 validTypesQuality
[t
] = a
.weight
;
98 return Object
.keys(validTypesQuality
).reduce((acc
, cur
) => {
99 // eslint-disable-next-line security/detect-object-injection
100 if (acc
=== undefined && validTypesQuality
[cur
] !== 0.0) {
103 // eslint-disable-next-line security/detect-object-injection
104 return validTypesQuality
[acc
] < validTypesQuality
[cur
] ? cur : acc
;
110 * Return all viable matches between acceptable and requested encodings, ordered by highest preference first.
111 * TODO: sort equal q-values by server-preference rather than header order
112 * @param {string[]} acceptableEncodings e.g. ['br', 'gzip'] in order of server preference
113 * @param {string} acceptHeader
115 static preferred(acceptableEncodings
, acceptHeader
) {
116 const Identity
= Enum
.EncodingType
.Identity
;
119 // Don't munge caller's list.
120 acceptableEncodings
= [...acceptableEncodings
];
122 // Server is always capable of identity encoding.
123 if (!(acceptableEncodings
.includes(Identity
))) {
124 acceptableEncodings
.push(Identity
);
127 const acceptClauses
= ContentNegotiation
._acceptClauses(acceptHeader
);
129 // Add identity as fallback clause if an Any type was not explicitly mentioned.
130 const acceptTypes
= acceptClauses
.map((enc
) => enc
.type
);
131 if (!(acceptTypes
.includes(Any
)) && !(acceptTypes
.includes(Identity
))) {
139 // Explicitly forbidden encodings will not be considered.
140 const forbidden
= acceptClauses
.filter((enc
) => enc
.weight
== 0).map((enc
) => enc
.type
);
142 acceptableEncodings
= acceptableEncodings
.filter((enc
) => {
143 // If * is forbidden, don't allow identity encoding.
144 const fallbackIsForbidden
= forbidden
.includes(Any
) && enc
=== Identity
&& !acceptTypes
.includes(Identity
);
146 const typeIsForbidden
= forbidden
.includes(enc
.type
);
147 return !typeIsForbidden
&& !fallbackIsForbidden
;
150 // Strip forbidden and unsupported from working set.
151 const allowedClauses
= acceptClauses
.filter((enc
) => {
152 const isAllowed
= enc
.weight
> 0;
153 const isSupported
= enc
.type
=== Any
|| acceptableEncodings
.includes(enc
.type
);
154 return isAllowed
&& isSupported
;
158 return allowedClauses
.map((enc
) => enc
.type
);
163 module
.exports
= ContentNegotiation
;