/*!1* content-disposition2* Copyright(c) 2014 Douglas Christopher Wilson3* MIT Licensed4*/56'use strict'78/**9* Module exports.10*/1112module.exports = contentDisposition13module.exports.parse = parse1415/**16* Module dependencies.17*/1819var basename = require('path').basename2021/**22* RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%")23*/2425var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex2627/**28* RegExp to match percent encoding escape.29*/3031var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/32var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g3334/**35* RegExp to match non-latin1 characters.36*/3738var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g3940/**41* RegExp to match quoted-pair in RFC 261642*43* quoted-pair = "\" CHAR44* CHAR = <any US-ASCII character (octets 0 - 127)>45*/4647var QESC_REGEXP = /\\([\u0000-\u007f])/g4849/**50* RegExp to match chars that must be quoted-pair in RFC 261651*/5253var QUOTE_REGEXP = /([\\"])/g5455/**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*/7879var 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-regex80var TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/81var TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/8283/**84* RegExp for various RFC 5987 grammar85*86* ext-value = charset "'" [ language ] "'" value-chars87* charset = "UTF-8" / "ISO-8859-1" / mime-charset88* mime-charset = 1*mime-charsetc89* mime-charsetc = ALPHA / DIGIT90* / "!" / "#" / "$" / "%" / "&"91* / "+" / "-" / "^" / "_" / "`"92* / "{" / "}" / "~"93* language = ( 2*3ALPHA [ extlang ] )94* / 4ALPHA95* / 5*8ALPHA96* extlang = *3( "-" 3ALPHA )97* value-chars = *( pct-encoded / attr-char )98* pct-encoded = "%" HEXDIG HEXDIG99* attr-char = ALPHA / DIGIT100* / "!" / "#" / "$" / "&" / "+" / "-" / "."101* / "^" / "_" / "`" / "|" / "~"102*/103104var 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!#$&+.^_`|~-])+)$/105106/**107* RegExp for various RFC 6266 grammar108*109* disposition-type = "inline" | "attachment" | disp-ext-type110* disp-ext-type = token111* disposition-parm = filename-parm | disp-ext-parm112* filename-parm = "filename" "=" value113* | "filename*" "=" ext-value114* disp-ext-parm = token "=" value115* | ext-token "=" ext-value116* ext-token = <the characters in token, followed by "*">117*/118119var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex120121/**122* Create an attachment Content-Disposition header.123*124* @param {string} [filename]125* @param {object} [options]126* @param {string} [options.type=attachment]127* @param {string|boolean} [options.fallback=true]128* @return {string}129* @api public130*/131132function contentDisposition (filename, options) {133var opts = options || {}134135// get type136var type = opts.type || 'attachment'137138// get parameters139var params = createparams(filename, opts.fallback)140141// format into string142return format(new ContentDisposition(type, params))143}144145/**146* Create parameters object from filename and fallback.147*148* @param {string} [filename]149* @param {string|boolean} [fallback=true]150* @return {object}151* @api private152*/153154function createparams (filename, fallback) {155if (filename === undefined) {156return157}158159var params = {}160161if (typeof filename !== 'string') {162throw new TypeError('filename must be a string')163}164165// fallback defaults to true166if (fallback === undefined) {167fallback = true168}169170if (typeof fallback !== 'string' && typeof fallback !== 'boolean') {171throw new TypeError('fallback must be a string or boolean')172}173174if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) {175throw new TypeError('fallback must be ISO-8859-1 string')176}177178// restrict to file base name179var name = basename(filename)180181// determine if name is suitable for quoted string182var isQuotedString = TEXT_REGEXP.test(name)183184// generate fallback name185var fallbackName = typeof fallback !== 'string'186? fallback && getlatin1(name)187: basename(fallback)188var hasFallback = typeof fallbackName === 'string' && fallbackName !== name189190// set extended filename parameter191if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) {192params['filename*'] = name193}194195// set filename parameter196if (isQuotedString || hasFallback) {197params.filename = hasFallback198? fallbackName199: name200}201202return params203}204205/**206* Format object to Content-Disposition header.207*208* @param {object} obj209* @param {string} obj.type210* @param {object} [obj.parameters]211* @return {string}212* @api private213*/214215function format (obj) {216var parameters = obj.parameters217var type = obj.type218219if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) {220throw new TypeError('invalid type')221}222223// start with normalized type224var string = String(type).toLowerCase()225226// append parameters227if (parameters && typeof parameters === 'object') {228var param229var params = Object.keys(parameters).sort()230231for (var i = 0; i < params.length; i++) {232param = params[i]233234var val = param.substr(-1) === '*'235? ustring(parameters[param])236: qstring(parameters[param])237238string += '; ' + param + '=' + val239}240}241242return string243}244245/**246* Decode a RFC 6987 field value (gracefully).247*248* @param {string} str249* @return {string}250* @api private251*/252253function decodefield (str) {254var match = EXT_VALUE_REGEXP.exec(str)255256if (!match) {257throw new TypeError('invalid extended field value')258}259260var charset = match[1].toLowerCase()261var encoded = match[2]262var value263264// to binary string265var binary = encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode)266267switch (charset) {268case 'iso-8859-1':269value = getlatin1(binary)270break271case 'utf-8':272value = new Buffer(binary, 'binary').toString('utf8')273break274default:275throw new TypeError('unsupported charset in extended field')276}277278return value279}280281/**282* Get ISO-8859-1 version of string.283*284* @param {string} val285* @return {string}286* @api private287*/288289function getlatin1 (val) {290// simple Unicode -> ISO-8859-1 transformation291return String(val).replace(NON_LATIN1_REGEXP, '?')292}293294/**295* Parse Content-Disposition header string.296*297* @param {string} string298* @return {object}299* @api private300*/301302function parse (string) {303if (!string || typeof string !== 'string') {304throw new TypeError('argument string is required')305}306307var match = DISPOSITION_TYPE_REGEXP.exec(string)308309if (!match) {310throw new TypeError('invalid type format')311}312313// normalize type314var index = match[0].length315var type = match[1].toLowerCase()316317var key318var names = []319var params = {}320var value321322// calculate index to start at323index = PARAM_REGEXP.lastIndex = match[0].substr(-1) === ';'324? index - 1325: index326327// match parameters328while ((match = PARAM_REGEXP.exec(string))) {329if (match.index !== index) {330throw new TypeError('invalid parameter format')331}332333index += match[0].length334key = match[1].toLowerCase()335value = match[2]336337if (names.indexOf(key) !== -1) {338throw new TypeError('invalid duplicate parameter')339}340341names.push(key)342343if (key.indexOf('*') + 1 === key.length) {344// decode extended value345key = key.slice(0, -1)346value = decodefield(value)347348// overwrite existing value349params[key] = value350continue351}352353if (typeof params[key] === 'string') {354continue355}356357if (value[0] === '"') {358// remove quotes and escapes359value = value360.substr(1, value.length - 2)361.replace(QESC_REGEXP, '$1')362}363364params[key] = value365}366367if (index !== -1 && index !== string.length) {368throw new TypeError('invalid parameter format')369}370371return new ContentDisposition(type, params)372}373374/**375* Percent decode a single character.376*377* @param {string} str378* @param {string} hex379* @return {string}380* @api private381*/382383function pdecode (str, hex) {384return String.fromCharCode(parseInt(hex, 16))385}386387/**388* Percent encode a single character.389*390* @param {string} char391* @return {string}392* @api private393*/394395function pencode (char) {396var hex = String(char)397.charCodeAt(0)398.toString(16)399.toUpperCase()400return hex.length === 1401? '%0' + hex402: '%' + hex403}404405/**406* Quote a string for HTTP.407*408* @param {string} val409* @return {string}410* @api private411*/412413function qstring (val) {414var str = String(val)415416return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'417}418419/**420* Encode a Unicode string for HTTP (RFC 5987).421*422* @param {string} val423* @return {string}424* @api private425*/426427function ustring (val) {428var str = String(val)429430// percent encode as UTF-8431var encoded = encodeURIComponent(str)432.replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode)433434return 'UTF-8\'\'' + encoded435}436437/**438* Class for parsed Content-Disposition header for v8 optimization439*/440441function ContentDisposition (type, parameters) {442this.type = type443this.parameters = parameters444}445446447