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
?.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(',')
51 .map((clause
) => ContentNegotiation
._unpackAcceptClause(clause
))
52 .filter((clause
) => clause
);
53 return clauses
.sort((a
, b
) => b
.weight
- a
.weight
);
57 * Check if an Accept-able Content-Type matches a fixed Content-Type.
58 * (Allows for '*' fields in Accept-able type.)
59 * @param {string} pattern
60 * @param {string} type
62 static _matchType(acceptableType
, fixedType
) {
63 acceptableType
= common
.splitFirst(acceptableType
, '/', '*');
64 fixedType
= common
.splitFirst(fixedType
, '/', '*');
65 for (let i
= 0; i
< acceptableType
.length
; i
++) {
66 // eslint-disable-next-line security/detect-object-injection
67 const v
= acceptableType
[i
];
68 // eslint-disable-next-line security/detect-object-injection
69 const f
= fixedType
[i
];
70 if (v
!== f
&& v
!== '*') {
78 * Return the best match between available and acceptable types.
79 * @param {string[]} acceptableTypes
80 * @param {string} acceptHeader
82 static accept(acceptableTypes
, acceptHeader
) {
83 const validTypesQuality
= {};
85 return acceptableTypes
[0];
87 // For each type offered in the header, from heaviest to lightest...
88 ContentNegotiation
._acceptClauses(acceptHeader
).forEach((a
) => {
89 // Consider each supported type...
90 acceptableTypes
.forEach((t
) => {
91 // Remember the heaviest weighting if it matches.
92 if (ContentNegotiation
._matchType(a
.type
, t
)
93 // eslint-disable-next-line security/detect-object-injection
94 && (!(t
in validTypesQuality
) || validTypesQuality
[t
] < a
.weight
)) {
95 // eslint-disable-next-line security/detect-object-injection
96 validTypesQuality
[t
] = a
.weight
;
100 return Object
.keys(validTypesQuality
).reduce((acc
, cur
) => {
101 // eslint-disable-next-line security/detect-object-injection
102 if (acc
=== undefined && validTypesQuality
[cur
] !== 0.0) {
105 // istanbul ignore next
106 // eslint-disable-next-line security/detect-object-injection
107 return validTypesQuality
[acc
] < validTypesQuality
[cur
] ? cur : acc
;
113 * Return all viable matches between acceptable and requested encodings, ordered by highest preference first.
114 * TODO: sort equal q-values by server-preference rather than header order
115 * @param {string[]} acceptableEncodings e.g. ['br', 'gzip'] in order of server preference
116 * @param {string} acceptHeader
118 static preferred(acceptableEncodings
, acceptHeader
) {
119 const Identity
= Enum
.EncodingType
.Identity
;
122 // Don't munge caller's list.
123 acceptableEncodings
= [...acceptableEncodings
];
125 // Server is always capable of identity encoding.
126 if (!(acceptableEncodings
.includes(Identity
))) {
127 acceptableEncodings
.push(Identity
);
130 const acceptClauses
= ContentNegotiation
._acceptClauses(acceptHeader
);
132 // Add identity as fallback clause if an Any type was not explicitly mentioned.
133 const acceptTypes
= acceptClauses
.map((enc
) => enc
.type
);
134 if (!(acceptTypes
.includes(Any
)) && !(acceptTypes
.includes(Identity
))) {
142 // Explicitly forbidden encodings will not be considered.
143 const forbidden
= acceptClauses
.filter((enc
) => enc
.weight
== 0).map((enc
) => enc
.type
);
145 acceptableEncodings
= acceptableEncodings
.filter((enc
) => {
146 // If * is forbidden, don't allow identity encoding.
147 const fallbackIsForbidden
= forbidden
.includes(Any
) && enc
=== Identity
&& !acceptTypes
.includes(Identity
);
149 const typeIsForbidden
= forbidden
.includes(enc
.type
);
150 return !typeIsForbidden
&& !fallbackIsForbidden
;
153 // Strip forbidden and unsupported from working set.
154 const allowedClauses
= acceptClauses
.filter((enc
) => {
155 const isAllowed
= enc
.weight
> 0;
156 const isSupported
= enc
.type
=== Any
|| acceptableEncodings
.includes(enc
.type
);
157 return isAllowed
&& isSupported
;
161 return allowedClauses
.map((enc
) => enc
.type
);
166 module
.exports
= ContentNegotiation
;