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;
11 * Methods for negotiating content types.
13 class ContentNegotiation
{
16 * Convert accept clause string to object.
17 * Adjust weight based on wildcards, to prefer literal matches.
18 * @param {string} clause portion of an accept header
19 * @returns {object|undefined} details of clause
21 static _unpackAcceptClause(clause
) {
22 let params
= clause
.split(';');
23 const type
= params
.shift().trim();
26 params
= params
.reduce((acc
, param
) => {
27 const [p
, v
] = common
.splitFirst(param
, '=').map((x
) => x
?.trim());
31 // Enforce max precision from spec, so that we can...
32 weight
= Number(weight
.toFixed(3));
33 // De-tune the quality slightly for wildcards.
34 const blur
= (type
.split('*').length
- 1) * WeightIota
;
39 // eslint-disable-next-line security/detect-object-injection
45 return { type
, weight
, params
};
50 * Split an accept field into clauses, return list of clauses sorted by heaviest weights first.
51 * @param {string} acceptHeader collection of accept clauses
52 * @returns {object[]} array of clause details sorted by desirability
54 static _acceptClauses(acceptHeader
) {
55 const clauses
= (acceptHeader
||'').split(',')
56 .map((clause
) => ContentNegotiation
._unpackAcceptClause(clause
))
57 .filter((clause
) => clause
);
58 return clauses
.sort((a
, b
) => b
.weight
- a
.weight
);
62 * Check if an Accept-able Content-Type matches a fixed Content-Type.
63 * (Allows for '*' fields in Accept-able type.)
64 * @param {string} acceptableType explicit or wildcard
65 * @param {string} fixedType explicit
66 * @returns {boolean} matches
68 static _matchType(acceptableType
, fixedType
) {
69 acceptableType
= common
.splitFirst(acceptableType
, '/', '*');
70 fixedType
= common
.splitFirst(fixedType
, '/', '*');
71 for (let i
= 0; i
< acceptableType
.length
; i
++) {
72 // eslint-disable-next-line security/detect-object-injection
73 const v
= acceptableType
[i
];
74 // eslint-disable-next-line security/detect-object-injection
75 const f
= fixedType
[i
];
76 if (v
!== f
&& v
!== '*') {
84 * Return the best match between available and acceptable types.
85 * @param {string[]} acceptableTypes available types
86 * @param {string} acceptHeader accept header
87 * @returns {string|undefined} best matched type
89 static accept(acceptableTypes
, acceptHeader
) {
90 const validTypesQuality
= {};
92 return acceptableTypes
[0];
94 // For each type offered in the header, from heaviest to lightest...
95 ContentNegotiation
._acceptClauses(acceptHeader
).forEach((a
) => {
96 // Consider each supported type...
97 acceptableTypes
.forEach((t
) => {
98 // Remember the heaviest weighting if it matches.
99 if (ContentNegotiation
._matchType(a
.type
, t
)
100 // eslint-disable-next-line security/detect-object-injection
101 && (!(t
in validTypesQuality
) || validTypesQuality
[t
] < a
.weight
)) {
102 // eslint-disable-next-line security/detect-object-injection
103 validTypesQuality
[t
] = a
.weight
;
107 return Object
.keys(validTypesQuality
).reduce((acc
, cur
) => {
108 // eslint-disable-next-line security/detect-object-injection
109 if (acc
=== undefined && validTypesQuality
[cur
] !== 0.0) {
112 // istanbul ignore next
113 // eslint-disable-next-line security/detect-object-injection
114 return validTypesQuality
[acc
] < validTypesQuality
[cur
] ? cur : acc
;
120 * Return all viable matches between acceptable and requested encodings, ordered by highest preference first.
121 * TODO: sort equal q-values by server-preference rather than header order
122 * @param {string[]} acceptableEncodings e.g. ['br', 'gzip'] in order of server preference
123 * @param {string} acceptHeader encoding header
124 * @returns {string[]} preferred encoding
126 static preferred(acceptableEncodings
, acceptHeader
) {
127 const Identity
= Enum
.EncodingType
.Identity
;
130 // Don't munge caller's list.
131 acceptableEncodings
= [...acceptableEncodings
];
133 // Server is always capable of identity encoding.
134 if (!(acceptableEncodings
.includes(Identity
))) {
135 acceptableEncodings
.push(Identity
);
138 const acceptClauses
= ContentNegotiation
._acceptClauses(acceptHeader
);
140 // Add identity as fallback clause if an Any type was not explicitly mentioned.
141 const acceptTypes
= acceptClauses
.map((enc
) => enc
.type
);
142 if (!(acceptTypes
.includes(Any
)) && !(acceptTypes
.includes(Identity
))) {
150 // Explicitly forbidden encodings will not be considered.
151 const forbidden
= acceptClauses
.filter((enc
) => enc
.weight
== 0).map((enc
) => enc
.type
);
153 acceptableEncodings
= acceptableEncodings
.filter((enc
) => {
154 // If * is forbidden, don't allow identity encoding.
155 const fallbackIsForbidden
= forbidden
.includes(Any
) && enc
=== Identity
&& !acceptTypes
.includes(Identity
);
157 const typeIsForbidden
= forbidden
.includes(enc
.type
);
158 return !typeIsForbidden
&& !fallbackIsForbidden
;
161 // Strip forbidden and unsupported from working set.
162 const allowedClauses
= acceptClauses
.filter((enc
) => {
163 const isAllowed
= enc
.weight
> 0;
164 const isSupported
= enc
.type
=== Any
|| acceptableEncodings
.includes(enc
.type
);
165 return isAllowed
&& isSupported
;
169 return allowedClauses
.map((enc
) => enc
.type
);
174 module
.exports
= ContentNegotiation
;