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