second
[josuexyz/.git] / node_modules / content-disposition / index.js
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 }