Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Avatar for KuCalc : devops.
Download
50640 views
1
/*!
2
* content-disposition
3
* Copyright(c) 2014 Douglas Christopher Wilson
4
* MIT Licensed
5
*/
6
7
'use strict'
8
9
/**
10
* Module exports.
11
*/
12
13
module.exports = contentDisposition
14
module.exports.parse = parse
15
16
/**
17
* Module dependencies.
18
*/
19
20
var basename = require('path').basename
21
22
/**
23
* RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%")
24
*/
25
26
var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex
27
28
/**
29
* RegExp to match percent encoding escape.
30
*/
31
32
var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/
33
var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g
34
35
/**
36
* RegExp to match non-latin1 characters.
37
*/
38
39
var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g
40
41
/**
42
* RegExp to match quoted-pair in RFC 2616
43
*
44
* quoted-pair = "\" CHAR
45
* CHAR = <any US-ASCII character (octets 0 - 127)>
46
*/
47
48
var QESC_REGEXP = /\\([\u0000-\u007f])/g
49
50
/**
51
* RegExp to match chars that must be quoted-pair in RFC 2616
52
*/
53
54
var QUOTE_REGEXP = /([\\"])/g
55
56
/**
57
* RegExp for various RFC 2616 grammar
58
*
59
* parameter = token "=" ( token | quoted-string )
60
* token = 1*<any CHAR except CTLs or separators>
61
* separators = "(" | ")" | "<" | ">" | "@"
62
* | "," | ";" | ":" | "\" | <">
63
* | "/" | "[" | "]" | "?" | "="
64
* | "{" | "}" | SP | HT
65
* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
66
* qdtext = <any TEXT except <">>
67
* quoted-pair = "\" CHAR
68
* CHAR = <any US-ASCII character (octets 0 - 127)>
69
* TEXT = <any OCTET except CTLs, but including LWS>
70
* LWS = [CRLF] 1*( SP | HT )
71
* CRLF = CR LF
72
* CR = <US-ASCII CR, carriage return (13)>
73
* LF = <US-ASCII LF, linefeed (10)>
74
* SP = <US-ASCII SP, space (32)>
75
* HT = <US-ASCII HT, horizontal-tab (9)>
76
* CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
77
* OCTET = <any 8-bit sequence of data>
78
*/
79
80
var PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g // eslint-disable-line no-control-regex
81
var TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/
82
var TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/
83
84
/**
85
* RegExp for various RFC 5987 grammar
86
*
87
* ext-value = charset "'" [ language ] "'" value-chars
88
* charset = "UTF-8" / "ISO-8859-1" / mime-charset
89
* mime-charset = 1*mime-charsetc
90
* mime-charsetc = ALPHA / DIGIT
91
* / "!" / "#" / "$" / "%" / "&"
92
* / "+" / "-" / "^" / "_" / "`"
93
* / "{" / "}" / "~"
94
* language = ( 2*3ALPHA [ extlang ] )
95
* / 4ALPHA
96
* / 5*8ALPHA
97
* extlang = *3( "-" 3ALPHA )
98
* value-chars = *( pct-encoded / attr-char )
99
* pct-encoded = "%" HEXDIG HEXDIG
100
* attr-char = ALPHA / DIGIT
101
* / "!" / "#" / "$" / "&" / "+" / "-" / "."
102
* / "^" / "_" / "`" / "|" / "~"
103
*/
104
105
var EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/
106
107
/**
108
* RegExp for various RFC 6266 grammar
109
*
110
* disposition-type = "inline" | "attachment" | disp-ext-type
111
* disp-ext-type = token
112
* disposition-parm = filename-parm | disp-ext-parm
113
* filename-parm = "filename" "=" value
114
* | "filename*" "=" ext-value
115
* disp-ext-parm = token "=" value
116
* | ext-token "=" ext-value
117
* ext-token = <the characters in token, followed by "*">
118
*/
119
120
var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex
121
122
/**
123
* Create an attachment Content-Disposition header.
124
*
125
* @param {string} [filename]
126
* @param {object} [options]
127
* @param {string} [options.type=attachment]
128
* @param {string|boolean} [options.fallback=true]
129
* @return {string}
130
* @api public
131
*/
132
133
function contentDisposition (filename, options) {
134
var opts = options || {}
135
136
// get type
137
var type = opts.type || 'attachment'
138
139
// get parameters
140
var params = createparams(filename, opts.fallback)
141
142
// format into string
143
return format(new ContentDisposition(type, params))
144
}
145
146
/**
147
* Create parameters object from filename and fallback.
148
*
149
* @param {string} [filename]
150
* @param {string|boolean} [fallback=true]
151
* @return {object}
152
* @api private
153
*/
154
155
function createparams (filename, fallback) {
156
if (filename === undefined) {
157
return
158
}
159
160
var params = {}
161
162
if (typeof filename !== 'string') {
163
throw new TypeError('filename must be a string')
164
}
165
166
// fallback defaults to true
167
if (fallback === undefined) {
168
fallback = true
169
}
170
171
if (typeof fallback !== 'string' && typeof fallback !== 'boolean') {
172
throw new TypeError('fallback must be a string or boolean')
173
}
174
175
if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) {
176
throw new TypeError('fallback must be ISO-8859-1 string')
177
}
178
179
// restrict to file base name
180
var name = basename(filename)
181
182
// determine if name is suitable for quoted string
183
var isQuotedString = TEXT_REGEXP.test(name)
184
185
// generate fallback name
186
var fallbackName = typeof fallback !== 'string'
187
? fallback && getlatin1(name)
188
: basename(fallback)
189
var hasFallback = typeof fallbackName === 'string' && fallbackName !== name
190
191
// set extended filename parameter
192
if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) {
193
params['filename*'] = name
194
}
195
196
// set filename parameter
197
if (isQuotedString || hasFallback) {
198
params.filename = hasFallback
199
? fallbackName
200
: name
201
}
202
203
return params
204
}
205
206
/**
207
* Format object to Content-Disposition header.
208
*
209
* @param {object} obj
210
* @param {string} obj.type
211
* @param {object} [obj.parameters]
212
* @return {string}
213
* @api private
214
*/
215
216
function format (obj) {
217
var parameters = obj.parameters
218
var type = obj.type
219
220
if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) {
221
throw new TypeError('invalid type')
222
}
223
224
// start with normalized type
225
var string = String(type).toLowerCase()
226
227
// append parameters
228
if (parameters && typeof parameters === 'object') {
229
var param
230
var params = Object.keys(parameters).sort()
231
232
for (var i = 0; i < params.length; i++) {
233
param = params[i]
234
235
var val = param.substr(-1) === '*'
236
? ustring(parameters[param])
237
: qstring(parameters[param])
238
239
string += '; ' + param + '=' + val
240
}
241
}
242
243
return string
244
}
245
246
/**
247
* Decode a RFC 6987 field value (gracefully).
248
*
249
* @param {string} str
250
* @return {string}
251
* @api private
252
*/
253
254
function decodefield (str) {
255
var match = EXT_VALUE_REGEXP.exec(str)
256
257
if (!match) {
258
throw new TypeError('invalid extended field value')
259
}
260
261
var charset = match[1].toLowerCase()
262
var encoded = match[2]
263
var value
264
265
// to binary string
266
var binary = encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode)
267
268
switch (charset) {
269
case 'iso-8859-1':
270
value = getlatin1(binary)
271
break
272
case 'utf-8':
273
value = new Buffer(binary, 'binary').toString('utf8')
274
break
275
default:
276
throw new TypeError('unsupported charset in extended field')
277
}
278
279
return value
280
}
281
282
/**
283
* Get ISO-8859-1 version of string.
284
*
285
* @param {string} val
286
* @return {string}
287
* @api private
288
*/
289
290
function getlatin1 (val) {
291
// simple Unicode -> ISO-8859-1 transformation
292
return String(val).replace(NON_LATIN1_REGEXP, '?')
293
}
294
295
/**
296
* Parse Content-Disposition header string.
297
*
298
* @param {string} string
299
* @return {object}
300
* @api private
301
*/
302
303
function parse (string) {
304
if (!string || typeof string !== 'string') {
305
throw new TypeError('argument string is required')
306
}
307
308
var match = DISPOSITION_TYPE_REGEXP.exec(string)
309
310
if (!match) {
311
throw new TypeError('invalid type format')
312
}
313
314
// normalize type
315
var index = match[0].length
316
var type = match[1].toLowerCase()
317
318
var key
319
var names = []
320
var params = {}
321
var value
322
323
// calculate index to start at
324
index = PARAM_REGEXP.lastIndex = match[0].substr(-1) === ';'
325
? index - 1
326
: index
327
328
// match parameters
329
while ((match = PARAM_REGEXP.exec(string))) {
330
if (match.index !== index) {
331
throw new TypeError('invalid parameter format')
332
}
333
334
index += match[0].length
335
key = match[1].toLowerCase()
336
value = match[2]
337
338
if (names.indexOf(key) !== -1) {
339
throw new TypeError('invalid duplicate parameter')
340
}
341
342
names.push(key)
343
344
if (key.indexOf('*') + 1 === key.length) {
345
// decode extended value
346
key = key.slice(0, -1)
347
value = decodefield(value)
348
349
// overwrite existing value
350
params[key] = value
351
continue
352
}
353
354
if (typeof params[key] === 'string') {
355
continue
356
}
357
358
if (value[0] === '"') {
359
// remove quotes and escapes
360
value = value
361
.substr(1, value.length - 2)
362
.replace(QESC_REGEXP, '$1')
363
}
364
365
params[key] = value
366
}
367
368
if (index !== -1 && index !== string.length) {
369
throw new TypeError('invalid parameter format')
370
}
371
372
return new ContentDisposition(type, params)
373
}
374
375
/**
376
* Percent decode a single character.
377
*
378
* @param {string} str
379
* @param {string} hex
380
* @return {string}
381
* @api private
382
*/
383
384
function pdecode (str, hex) {
385
return String.fromCharCode(parseInt(hex, 16))
386
}
387
388
/**
389
* Percent encode a single character.
390
*
391
* @param {string} char
392
* @return {string}
393
* @api private
394
*/
395
396
function pencode (char) {
397
var hex = String(char)
398
.charCodeAt(0)
399
.toString(16)
400
.toUpperCase()
401
return hex.length === 1
402
? '%0' + hex
403
: '%' + hex
404
}
405
406
/**
407
* Quote a string for HTTP.
408
*
409
* @param {string} val
410
* @return {string}
411
* @api private
412
*/
413
414
function qstring (val) {
415
var str = String(val)
416
417
return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
418
}
419
420
/**
421
* Encode a Unicode string for HTTP (RFC 5987).
422
*
423
* @param {string} val
424
* @return {string}
425
* @api private
426
*/
427
428
function ustring (val) {
429
var str = String(val)
430
431
// percent encode as UTF-8
432
var encoded = encodeURIComponent(str)
433
.replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode)
434
435
return 'UTF-8\'\'' + encoded
436
}
437
438
/**
439
* Class for parsed Content-Disposition header for v8 optimization
440
*/
441
442
function ContentDisposition (type, parameters) {
443
this.type = type
444
this.parameters = parameters
445
}
446
447