AK-21/Graphite-Industrial-Intelligence
0
1/*!2 * content-disposition3 * Copyright(c) 2014-2017 Douglas Christopher Wilson4 * MIT Licensed5 */6 7'use strict'8 9/**10 * Module exports.11 * @public12 */13 14module.exports = contentDisposition15module.exports.parse = parse16 17/**18 * TextDecoder instance for UTF-8 decoding when decodeURIComponent fails due to invalid byte sequences.19 * @type {TextDecoder}20 * @private21 */22const utf8Decoder = new TextDecoder('utf-8')23 24/**25 * RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%")26 * @private27 */28 29var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex30 31/**32 * RegExp to match non-latin1 characters.33 * @private34 */35 36var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g37 38/**39 * RegExp to match quoted-pair in RFC 261640 *41 * quoted-pair = "\" CHAR42 * CHAR = <any US-ASCII character (octets 0 - 127)>43 * @private44 */45 46var QESC_REGEXP = /\\([\u0000-\u007f])/g // eslint-disable-line no-control-regex47 48/**49 * RegExp to match chars that must be quoted-pair in RFC 261650 * @private51 */52 53var QUOTE_REGEXP = /([\\"])/g54 55/**56 * RegExp for various RFC 2616 grammar57 *58 * parameter = token "=" ( token | quoted-string )59 * token = 1*<any CHAR except CTLs or separators>60 * separators = "(" | ")" | "<" | ">" | "@"61 * | "," | ";" | ":" | "\" | <">62 * | "/" | "[" | "]" | "?" | "="63 * | "{" | "}" | SP | HT64 * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )65 * qdtext = <any TEXT except <">>66 * quoted-pair = "\" CHAR67 * CHAR = <any US-ASCII character (octets 0 - 127)>68 * TEXT = <any OCTET except CTLs, but including LWS>69 * LWS = [CRLF] 1*( SP | HT )70 * CRLF = CR LF71 * CR = <US-ASCII CR, carriage return (13)>72 * LF = <US-ASCII LF, linefeed (10)>73 * SP = <US-ASCII SP, space (32)>74 * HT = <US-ASCII HT, horizontal-tab (9)>75 * CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>76 * OCTET = <any 8-bit sequence of data>77 * @private78 */79 80var 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-regex81var TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/82var TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/83 84/**85 * RegExp for various RFC 5987 grammar86 *87 * ext-value = charset "'" [ language ] "'" value-chars88 * charset = "UTF-8" / "ISO-8859-1" / mime-charset89 * mime-charset = 1*mime-charsetc90 * mime-charsetc = ALPHA / DIGIT91 * / "!" / "#" / "$" / "%" / "&"92 * / "+" / "-" / "^" / "_" / "`"93 * / "{" / "}" / "~"94 * language = ( 2*3ALPHA [ extlang ] )95 * / 4ALPHA96 * / 5*8ALPHA97 * extlang = *3( "-" 3ALPHA )98 * value-chars = *( pct-encoded / attr-char )99 * pct-encoded = "%" HEXDIG HEXDIG100 * attr-char = ALPHA / DIGIT101 * / "!" / "#" / "$" / "&" / "+" / "-" / "."102 * / "^" / "_" / "`" / "|" / "~"103 * @private104 */105 106var 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!#$&+.^_`|~-])+)$/107 108/**109 * RegExp for various RFC 6266 grammar110 *111 * disposition-type = "inline" | "attachment" | disp-ext-type112 * disp-ext-type = token113 * disposition-parm = filename-parm | disp-ext-parm114 * filename-parm = "filename" "=" value115 * | "filename*" "=" ext-value116 * disp-ext-parm = token "=" value117 * | ext-token "=" ext-value118 * ext-token = <the characters in token, followed by "*">119 * @private120 */121 122var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex123 124/**125 * Create an attachment Content-Disposition header.126 *127 * @param {string} [filename]128 * @param {object} [options]129 * @param {string} [options.type=attachment]130 * @param {string|boolean} [options.fallback=true]131 * @return {string}132 * @public133 */134 135function contentDisposition (filename, options) {136 var opts = options || {}137 138 // get type139 var type = opts.type || 'attachment'140 141 // get parameters142 var params = createparams(filename, opts.fallback)143 144 // format into string145 return format(new ContentDisposition(type, params))146}147 148/**149 * Create parameters object from filename and fallback.150 *151 * @param {string} [filename]152 * @param {string|boolean} [fallback=true]153 * @return {object}154 * @private155 */156 157function createparams (filename, fallback) {158 if (filename === undefined) {159 return160 }161 162 var params = {}163 164 if (typeof filename !== 'string') {165 throw new TypeError('filename must be a string')166 }167 168 // fallback defaults to true169 if (fallback === undefined) {170 fallback = true171 }172 173 if (typeof fallback !== 'string' && typeof fallback !== 'boolean') {174 throw new TypeError('fallback must be a string or boolean')175 }176 177 if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) {178 throw new TypeError('fallback must be ISO-8859-1 string')179 }180 181 // restrict to file base name182 var name = basename(filename)183 184 // determine if name is suitable for quoted string185 var isQuotedString = TEXT_REGEXP.test(name)186 187 // generate fallback name188 var fallbackName = typeof fallback !== 'string'189 ? fallback && getlatin1(name)190 : basename(fallback)191 var hasFallback = typeof fallbackName === 'string' && fallbackName !== name192 193 // set extended filename parameter194 if (hasFallback || !isQuotedString || hasHexEscape(name)) {195 params['filename*'] = name196 }197 198 // set filename parameter199 if (isQuotedString || hasFallback) {200 params.filename = hasFallback201 ? fallbackName202 : name203 }204 205 return params206}207 208/**209 * Format object to Content-Disposition header.210 *211 * @param {object} obj212 * @param {string} obj.type213 * @param {object} [obj.parameters]214 * @return {string}215 * @private216 */217 218function format (obj) {219 var parameters = obj.parameters220 var type = obj.type221 222 if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) {223 throw new TypeError('invalid type')224 }225 226 // start with normalized type227 var string = String(type).toLowerCase()228 229 // append parameters230 if (parameters && typeof parameters === 'object') {231 var param232 var params = Object.keys(parameters).sort()233 234 for (var i = 0; i < params.length; i++) {235 param = params[i]236 237 var val = param.slice(-1) === '*'238 ? ustring(parameters[param])239 : qstring(parameters[param])240 241 string += '; ' + param + '=' + val242 }243 }244 245 return string246}247 248/**249 * Decode a RFC 5987 field value (gracefully).250 *251 * @param {string} str252 * @return {string}253 * @private254 */255 256function decodefield (str) {257 const match = EXT_VALUE_REGEXP.exec(str)258 259 if (!match) {260 throw new TypeError('invalid extended field value')261 }262 263 const charset = match[1].toLowerCase()264 const encoded = match[2]265 266 switch (charset) {267 case 'iso-8859-1':268 {269 const binary = decodeHexEscapes(encoded)270 return getlatin1(binary)271 }272 case 'utf-8':273 case 'utf8':274 {275 try {276 return decodeURIComponent(encoded)277 } catch {278 // Failed to decode with decodeURIComponent, fallback to lenient decoding which replaces invalid UTF-8 byte sequences with the Unicode replacement character279 // TODO: Consider removing in the next major version to be more strict about invalid percent-encodings280 const binary = decodeHexEscapes(encoded)281 282 const bytes = new Uint8Array(binary.length)283 for (let idx = 0; idx < binary.length; idx++) {284 bytes[idx] = binary.charCodeAt(idx)285 }286 287 return utf8Decoder.decode(bytes)288 }289 }290 }291 throw new TypeError('unsupported charset in extended field')292}293 294/**295 * Get ISO-8859-1 version of string.296 *297 * @param {string} val298 * @return {string}299 * @private300 */301 302function getlatin1 (val) {303 // simple Unicode -> ISO-8859-1 transformation304 return String(val).replace(NON_LATIN1_REGEXP, '?')305}306 307/**308 * Parse Content-Disposition header string.309 *310 * @param {string} string311 * @return {object}312 * @public313 */314 315function parse (string) {316 if (!string || typeof string !== 'string') {317 throw new TypeError('argument string is required')318 }319 320 var match = DISPOSITION_TYPE_REGEXP.exec(string)321 322 if (!match) {323 throw new TypeError('invalid type format')324 }325 326 // normalize type327 var index = match[0].length328 var type = match[1].toLowerCase()329 330 var key331 var names = []332 var params = {}333 var value334 335 // calculate index to start at336 index = PARAM_REGEXP.lastIndex = match[0].slice(-1) === ';'337 ? index - 1338 : index339 340 // match parameters341 while ((match = PARAM_REGEXP.exec(string))) {342 if (match.index !== index) {343 throw new TypeError('invalid parameter format')344 }345 346 index += match[0].length347 key = match[1].toLowerCase()348 value = match[2]349 350 if (names.indexOf(key) !== -1) {351 throw new TypeError('invalid duplicate parameter')352 }353 354 names.push(key)355 356 if (key.indexOf('*') + 1 === key.length) {357 // decode extended value358 key = key.slice(0, -1)359 value = decodefield(value)360 361 // overwrite existing value362 params[key] = value363 continue364 }365 366 if (typeof params[key] === 'string') {367 continue368 }369 370 if (value[0] === '"') {371 // remove quotes and escapes372 value = value373 .slice(1, -1)374 .replace(QESC_REGEXP, '$1')375 }376 377 params[key] = value378 }379 380 if (index !== -1 && index !== string.length) {381 throw new TypeError('invalid parameter format')382 }383 384 return new ContentDisposition(type, params)385}386 387/**388 * Percent encode a single character.389 *390 * @param {string} char391 * @return {string}392 * @private393 */394 395function pencode (char) {396 return '%' + String(char)397 .charCodeAt(0)398 .toString(16)399 .toUpperCase()400}401 402/**403 * Quote a string for HTTP.404 *405 * @param {string} val406 * @return {string}407 * @private408 */409 410function qstring (val) {411 var str = String(val)412 413 return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'414}415 416/**417 * Encode a Unicode string for HTTP (RFC 5987).418 *419 * @param {string} val420 * @return {string}421 * @private422 */423 424function ustring (val) {425 var str = String(val)426 427 // percent encode as UTF-8428 var encoded = encodeURIComponent(str)429 .replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode)430 431 return 'UTF-8\'\'' + encoded432}433 434/**435 * Class for parsed Content-Disposition header for v8 optimization436 *437 * @public438 * @param {string} type439 * @param {object} parameters440 * @constructor441 */442 443function ContentDisposition (type, parameters) {444 this.type = type445 this.parameters = parameters446}447 448/**449 * Return the last portion of a path450 *451 * @param {string} path452 * @returns {string}453 */454function basename (path) {455 const normalized = path.replaceAll('\\', '/')456 457 let end = normalized.length458 while (end > 0 && normalized[end - 1] === '/') {459 end--460 }461 462 if (end === 0) {463 return ''464 }465 466 let start = end - 1467 while (start >= 0 && normalized[start] !== '/') {468 start--469 }470 471 return normalized.slice(start + 1, end)472}473 474/**475 * Check if a character is a hex digit [0-9A-Fa-f]476 *477 * @param {string} char478 * @return {boolean}479 * @private480 */481function isHexDigit (char) {482 const code = char.charCodeAt(0)483 return (484 (code >= 48 && code <= 57) || // 0-9485 (code >= 65 && code <= 70) || // A-F486 (code >= 97 && code <= 102) // a-f487 )488}489 490/**491 * Check if a string contains percent encoding escapes.492 *493 * @param {string} str494 * @return {boolean}495 * @private496 */497function hasHexEscape (str) {498 const maxIndex = str.length - 3499 let lastIndex = -1500 501 while ((lastIndex = str.indexOf('%', lastIndex + 1)) !== -1 && lastIndex <= maxIndex) {502 if (isHexDigit(str[lastIndex + 1]) && isHexDigit(str[lastIndex + 2])) {503 return true504 }505 }506 507 return false508}509 510/**511 * Decode hex escapes in a string (e.g., %20 -> space)512 *513 * @param {string} str514 * @return {string}515 * @private516 */517function decodeHexEscapes (str) {518 const firstEscape = str.indexOf('%')519 if (firstEscape === -1) return str520 521 let result = str.slice(0, firstEscape)522 for (let idx = firstEscape; idx < str.length; idx++) {523 if (524 str[idx] === '%' &&525 idx + 2 < str.length &&526 isHexDigit(str[idx + 1]) &&527 isHexDigit(str[idx + 2])528 ) {529 result += String.fromCharCode(Number.parseInt(str[idx + 1] + str[idx + 2], 16))530 idx += 2531 } else {532 result += str[idx]533 }534 }535 return result536}537 