twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

index.js (10594B)


      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 }