index.js
1 /*! 2 * type-is 3 * Copyright(c) 2014 Jonathan Ong 4 * Copyright(c) 2014-2015 Douglas Christopher Wilson 5 * MIT Licensed 6 */ 7 8 'use strict' 9 10 /** 11 * Module dependencies. 12 * @private 13 */ 14 15 var contentType = require('content-type') 16 var mime = require('mime-types') 17 var typer = require('media-typer') 18 19 /** 20 * Module exports. 21 * @public 22 */ 23 24 module.exports = typeofrequest 25 module.exports.is = typeis 26 module.exports.hasBody = hasbody 27 module.exports.normalize = normalize 28 module.exports.match = mimeMatch 29 30 /** 31 * Compare a `value` content-type with `types`. 32 * Each `type` can be an extension like `html`, 33 * a special shortcut like `multipart` or `urlencoded`, 34 * or a mime type. 35 * 36 * If no types match, `false` is returned. 37 * Otherwise, the first `type` that matches is returned. 38 * 39 * @param {String} value 40 * @param {Array} types 41 * @public 42 */ 43 44 function typeis (value, types_) { 45 var i 46 var types = types_ 47 48 // remove parameters and normalize 49 var val = tryNormalizeType(value) 50 51 // no type or invalid 52 if (!val) { 53 return false 54 } 55 56 // support flattened arguments 57 if (types && !Array.isArray(types)) { 58 types = new Array(arguments.length - 1) 59 for (i = 0; i < types.length; i++) { 60 types[i] = arguments[i + 1] 61 } 62 } 63 64 // no types, return the content type 65 if (!types || !types.length) { 66 return val 67 } 68 69 var type 70 for (i = 0; i < types.length; i++) { 71 if (mimeMatch(normalize(type = types[i]), val)) { 72 return type[0] === '+' || type.indexOf('*') !== -1 73 ? val 74 : type 75 } 76 } 77 78 // no matches 79 return false 80 } 81 82 /** 83 * Check if a request has a request body. 84 * A request with a body __must__ either have `transfer-encoding` 85 * or `content-length` headers set. 86 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 87 * 88 * @param {Object} request 89 * @return {Boolean} 90 * @public 91 */ 92 93 function hasbody (req) { 94 return req.headers['transfer-encoding'] !== undefined || 95 !isNaN(req.headers['content-length']) 96 } 97 98 /** 99 * Check if the incoming request contains the "Content-Type" 100 * header field, and it contains any of the give mime `type`s. 101 * If there is no request body, `null` is returned. 102 * If there is no content type, `false` is returned. 103 * Otherwise, it returns the first `type` that matches. 104 * 105 * Examples: 106 * 107 * // With Content-Type: text/html; charset=utf-8 108 * this.is('html'); // => 'html' 109 * this.is('text/html'); // => 'text/html' 110 * this.is('text/*', 'application/json'); // => 'text/html' 111 * 112 * // When Content-Type is application/json 113 * this.is('json', 'urlencoded'); // => 'json' 114 * this.is('application/json'); // => 'application/json' 115 * this.is('html', 'application/*'); // => 'application/json' 116 * 117 * this.is('html'); // => false 118 * 119 * @param {Object} req 120 * @param {(String|Array)} types... 121 * @return {(String|false|null)} 122 * @public 123 */ 124 125 function typeofrequest (req, types_) { 126 // no body 127 if (!hasbody(req)) return null 128 // support flattened arguments 129 var types = arguments.length > 2 130 ? Array.prototype.slice.call(arguments, 1) 131 : types_ 132 // request content type 133 var value = req.headers['content-type'] 134 135 return typeis(value, types) 136 } 137 138 /** 139 * Normalize a mime type. 140 * If it's a shorthand, expand it to a valid mime type. 141 * 142 * In general, you probably want: 143 * 144 * var type = is(req, ['urlencoded', 'json', 'multipart']); 145 * 146 * Then use the appropriate body parsers. 147 * These three are the most common request body types 148 * and are thus ensured to work. 149 * 150 * @param {String} type 151 * @return {String|false|null} 152 * @public 153 */ 154 155 function normalize (type) { 156 if (typeof type !== 'string') { 157 // invalid type 158 return false 159 } 160 161 switch (type) { 162 case 'urlencoded': 163 return 'application/x-www-form-urlencoded' 164 case 'multipart': 165 return 'multipart/*' 166 } 167 168 if (type[0] === '+') { 169 // "+json" -> "*/*+json" expando 170 return '*/*' + type 171 } 172 173 return type.indexOf('/') === -1 174 ? mime.lookup(type) 175 : type 176 } 177 178 /** 179 * Check if `expected` mime type 180 * matches `actual` mime type with 181 * wildcard and +suffix support. 182 * 183 * @param {String} expected 184 * @param {String} actual 185 * @return {Boolean} 186 * @public 187 */ 188 189 function mimeMatch (expected, actual) { 190 // invalid type 191 if (expected === false) { 192 return false 193 } 194 195 // split types 196 var actualParts = actual.split('/') 197 var expectedParts = expected.split('/') 198 199 // invalid format 200 if (actualParts.length !== 2 || expectedParts.length !== 2) { 201 return false 202 } 203 204 // validate type 205 if (expectedParts[0] !== '*' && expectedParts[0] !== actualParts[0]) { 206 return false 207 } 208 209 // validate suffix wildcard 210 if (expectedParts[1].slice(0, 2) === '*+') { 211 return expectedParts[1].length <= actualParts[1].length + 1 && 212 expectedParts[1].slice(1) === actualParts[1].slice(1 - expectedParts[1].length) 213 } 214 215 // validate subtype 216 if (expectedParts[1] !== '*' && expectedParts[1] !== actualParts[1]) { 217 return false 218 } 219 220 return true 221 } 222 223 /** 224 * Normalize a type and remove parameters. 225 * 226 * @param {string} value 227 * @return {(string|null)} 228 * @private 229 */ 230 function normalizeType (value) { 231 // Parse the type 232 var type = contentType.parse(value).type 233 234 return typer.test(type) ? type : null 235 } 236 237 /** 238 * Try to normalize a type and remove parameters. 239 * 240 * @param {string} value 241 * @return {(string|null)} 242 * @private 243 */ 244 function tryNormalizeType (value) { 245 try { 246 return value ? normalizeType(value) : null 247 } catch (err) { 248 return null 249 } 250 }