UNPKG

@adonisjs/framework

Version:

Adonis framework makes it easy for you to write webapps with less code

904 lines (830 loc) 19.4 kB
'use strict' /* * adonis-framework * * (c) Harminder Virk <virk@adonisjs.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * The Trust value is used to instruct Adonis whether or not to * trust **Proxy** specific specific. The value should be set * to true, whenever your server is behind a proxy server * like nginx. * * The value is read from the `app.js` file under config directory. * Check [proxy-addr](https://www.npmjs.com/package/proxy-addr) * to find list of all available inputs for the trust * property. * * @property Trust * @type {String|Boolean|Function|Array} */ const _ = require('lodash') const nodeReq = require('node-req') const nodeCookie = require('node-cookie') const pathToRegexp = require('path-to-regexp') const useragent = require('useragent') const Macroable = require('macroable') const debug = require('debug')('adonis:request') const SUBDOMAIN_OFFSET = 'app.http.subdomainOffset' const TRUST_PROXY = 'app.http.trustProxy' const SECRET = 'app.appKey' /** * A facade over Node.js HTTP `req` object, making it * easier and simpler to access request information. * You can access the original **req** object as * `request.request` * * @binding Adonis/Src/Request * @group Http * * @class Request * @constructor */ class Request extends Macroable { constructor (request, response, Config) { super() /** * Reference to native HTTP request object * * @attribute request * @type {Object} */ this.request = request /** * Reference to route params. This will be set by server * automatically once the route has been resolved. * * @attribute params * @type {Object} */ this.params = {} /** * Reference to native HTTP response object * * @attribute response * @type {Object} */ this.response = response /** * Reference to config provider to read * http specific settings. * * @attribute Config * @type {Object} */ this.Config = Config /** * The qs object * * @type {Object} */ this._qs = null /** * Reference to request body * * @type {Object} */ this._body = null /** * Reference to raw body * * @type {Object} */ this._raw = null /** * A merged object of get and post. This will re-computed everytime we will * update the `qs` or `body` properties on this class. * * @type {Object} */ this._all = null /** * A reference to the original request object. The object will be freezed for further * modifications once computed * * @type {Object} */ this._original = {} /** * Tracking whether initial qs and body has been * set or not * * @type {Object} */ this._originalCalls = { qs: false, body: false } /** * Set qs by parsing the request. `this._all` will be computed out of it */ this.qs = nodeReq.get(this.request) } /** * Request body * * @method body * * @return {Object} */ get body () { return this._body || {} } /** * Mutate request body, this method will * mutate the `all` object as well * * @method body * * @param {Object} body * * @return {void} */ set body (body) { debug('updated request body') this._body = body this._all = _.merge({}, this.get(), body) if (!this._originalCalls.body) { this._originalCalls.body = true this._updateRequestOriginal() } } /** * Returns the query string as an object * * @method qs * * @return {Objec} */ get qs () { return this._qs || {} } /** * Update the query string. This will re-compute the * _all * * @method qs * * @param {Object} qs * * @return {void} */ set qs (qs) { debug('updated request query string') this._qs = qs this._all = _.merge({}, this.post(), qs) if (!this._originalCalls.qs) { this._originalCalls.qs = true this._updateRequestOriginal() } } /** * Updates the request original payload by tracking the * amount of mutations made to it. Once `qs` and `body` * is set for the first time, after that original * object will be freexed * * @method _updateRequestOriginal * * @return {void} * * @private */ _updateRequestOriginal () { if (Object.isFrozen(this._original)) { return } /** * Update original value */ debug('updated request original data') this._original = _.merge({}, this._all) /** * Once qs and body is stable, we will freeze the original * object. This is important, since the original request * body is mutable, however a reference to original is * must */ if (this._originalCalls.qs && this._originalCalls.body) { debug('freezing request original data') Object.freeze(this._original) } } /** * Returns a boolean indicating if user is a bad safari. * This method is used by the `fresh` method to address * a known bug in safari described [here](http://tech.vg.no/2013/10/02/ios7-bug-shows-white-page-when-getting-304-not-modified-from-server/) * * @method _isBadSafari * * @return {Boolean} * * @private */ _isBadSafari () { const ua = this.header('user-agent') const cc = this.header('cache-control') return (useragent.is(ua).safari || useragent.is(ua).mobile_safari) && cc === 'max-age=0' } /** * Returns query params from HTTP url. * * @method get * * @return {Object} * * @example * ```js * request.get() * ``` */ get () { return this.qs } /** * Returns an object of request body. This method * does not parses the request body and instead * depends upon the body parser middleware * to set the private `_body` property. * * No it's not against the law of programming, since AdonisJs * by default is shipped with body parser middleware. * * @method post * * @return {Object} * * @example * ```js * request.body() * ``` */ post () { return this.body } /** * Similar to `request.all`, but later mutations are avoided. Use this * method, when you want to read the values submitted in the original * HTTP request. * * @method original * * @return {Object} */ original () { return this._original } /** * Returns an object after merging {{#crossLink "Request/get"}}{{/crossLink}} and * {{#crossLink "Request/post"}}{{/crossLink}} values * * @method all * * @return {Object} * * @example * ```js * request.all() * ``` */ all () { return this._all } /** * Returns request raw body * * @method raw * * @return {Object} */ raw () { return this._raw } /** * Returns an array of key/value pairs for the defined keys. * This method is super helpful when your HTML forms sends * an array of values and you want them as individual * objects to be saved directly via Lucid models. * * # Note * This method always returns a stable array by setting value for * `undefined` keys to `null`. For example your data payload has * 3 emails and 2 usernames, the final array will have 3 * objects with all the emails and the last object will * have `username` set to `null`. * * @method collect * * @param {Array} keys * * @return {Array} * * @example * ```js * // data {username: ['virk', 'nikk'], age: [26, 25]} * const users = request.collect(['username', 'age']) * // returns [{username: 'virk', age: 26}, {username: 'nikk', age: 25}] * ``` */ collect (keys) { /** * Making sure to wrap strings as an array. */ const selectedValues = _(this.only(keys)).values().map((value) => { return Array.isArray(value) ? value : [value] }).value() const values = _.zip.apply(_, selectedValues) return _.map(values, (item, index) => { return _.transform(keys, (result, k, i) => { result[keys[i]] = item[i] || null return result }, {}) }) } /** * Returns the value from the request body or * query string, but only for a single key. * * @method input * * @param {String} key * @param {Mixed} [defaultValue] * * @return {Mixed} Actual value or the default value falling back to `null` */ input (key, defaultValue) { return _.get(this.all(), key, defaultValue) } /** * Returns everything from request body and query * string except the given keys. * * @param {Array} keys * * @method except * * @return {Object} * * @example * ```js * request.except(['username', 'age']) * ``` */ except (keys) { return _.omit(this.all(), keys) } /** * Returns value for only given keys. * * @method only * * @param {Array} keys * * @return {Object} * * @example * ```js * request.only(['username', 'age']) * ``` */ only (keys) { return _.pick(this.all(), keys) } /** * Returns the http request method, it will give preference * to spoofed method when `http.allowMethodSpoofing` is * enabled inside the `config/app.js` file. * * Make use of {{#crossLink "Request/intended"}}{{/crossLink}} to * get the actual method. * * @method method * * @return {String} Request method always in uppercase */ method () { if (!this.Config.get('app.http.allowMethodSpoofing') || this.intended() !== 'POST') { return this.intended() } const method = this.input('_method', this.intended()) return method.toUpperCase() } /** * Returns the intended method for HTTP request. This method * is useful when you have method spoofing enabled and wants * the actual request method. * * @method intended * * @return {String} Request method always in uppercase */ intended () { return nodeReq.method(this.request) } /** * Returns HTTP request headers. * * @method headers * * @return {Object} */ headers () { return nodeReq.headers(this.request) } /** * Returns header value for a given key. * * @method header * * @param {String} key * @param {Mixed} [defaultValue] * * @return {Mixed} Actual value or the default value, falling back to `null` */ header (key, defaultValue) { return nodeReq.header(this.request, key) || defaultValue } /** * Returns the most trusted ip address for a given * HTTP request. * * @method ip * * @param {Trust} [trust = Config.get('app.http.trustProxy')] * * @return {String} */ ip (trust = this.Config.get(TRUST_PROXY)) { return nodeReq.ip(this.request, trust) } /** * Returns an array of ips from most to the least trust one. * It will remove the default ip address, which can be * accessed via `ip` method. * * Also when trust is set to true, It will look into `X-Forwaded-For` * header to pull the ip address set by client or your proxy server. * * @method ips * * @param {Trust} [trust = Config.get('app.http.trustProxy')] * * @return {Array} */ ips (trust = this.Config.get(TRUST_PROXY)) { return nodeReq.ips(this.request, trust) } /** * Returns the protocol for the request. * * @method protocol * * @param {Trust} [trust = Config.get('app.http.trustProxy')] * * @return {String} */ protocol (trust = this.Config.get(TRUST_PROXY)) { return nodeReq.protocol(this.request, trust) } /** * Returns a boolean indicating whether request is * on https or not * * @method secure * * @return {Boolean} */ secure () { return nodeReq.secure(this.request) } /** * Returns an array of subdomains. It will exclude `www` * from the list. * * @method subdomains * * @param {Trust} [trust = Config.get('app.http.trustProxy')] * @param {Number} [offset = Config.get('app.http.subdomainOffset')] * * @return {Array} */ subdomains (trust = this.Config.get(TRUST_PROXY, false), offset = this.Config.get(SUBDOMAIN_OFFSET, 2)) { return nodeReq.subdomains(this.request, trust, offset) } /** * Returns a boolean indicating whether request * is ajax or not. * * @method ajax * * @return {Boolean} */ ajax () { return nodeReq.ajax(this.request) } /** * Returns a boolean indicating whether request * is pjax or not. * * @method pjax * * @return {Boolean} */ pjax () { return nodeReq.pjax(this.request) } /** * Returns the hostname for the request * * @method hostname * * @param {Mixed} [trust = Config.get('app.http.trustProxy')] * * @return {String} */ hostname (trust = this.Config.get(TRUST_PROXY, false)) { return nodeReq.hostname(this.request, trust) } /** * Returns url without query string for the HTTP request. * * @method url * * @return {String} */ url () { return nodeReq.url(this.request) } /** * Returns originalUrl for the HTTP request. * * @method originalUrl * * @return {String} */ originalUrl () { return nodeReq.originalUrl(this.request) } /** * Check the request body type based upon http * `Content-type` header. * * @method is * * @param {Array} [types] * * @return {String} * * @example * ```js * // request.headers.content-type = 'application/json' * * request.is(['json']) // json * request.is(['json', 'html']) // json * request.is(['application/*']) // application/json * * request.is(['html']) // '<empty string>' * ``` */ is (types) { return nodeReq.is(this.request, types) } /** * Returns the best accepted response type based from * the `Accept` header. If no `types` are provided * the return value will be array containing all * the `Accept` header values. * * @method accepts * * @param {Array} [types] * * @return {String|Array} */ accepts (types) { return nodeReq.accepts(this.request, types) || '' } /** * Similar to `accepts`, but always returns an array of * values from `Accept` header, starting from most * preferred from least. * * @method types * * @return {Array} */ types () { return nodeReq.types(this.request) } /** * Returns request language based upon HTTP `Accept-Language` * header. This method will filter from the list of * acceptedLanguages array. * * @method language * * @param {Array} [acceptedLanguages] * * @return {String} */ language (acceptedLanguages) { return nodeReq.language(this.request, acceptedLanguages) } /** * Returns an array of request languages based on HTTP `Accept-Language` * header. * * @method languages * * @return {Array} */ languages () { return nodeReq.languages(this.request) } /** * Returns most preferred encoding based upon `Accept-Encoding` * header. This method will filter encodings based upon on * the acceptedEncodings string * * @method encoding * * @param {Array} [acceptedEncodings] * * @return {String} */ encoding (acceptedEncodings) { return nodeReq.encoding(this.request, acceptedEncodings) } /** * Returns an array of encodings based upon `Accept-Encoding` * header. * * @method encodings * * @return {Array} */ encodings () { return nodeReq.encodings(this.request) } /** * Returns most preferred charset based upon the `Accept-Charset` * header. This method will filter from the list of acceptedCharsets * parameter. * * @method charset * * @param {Array} acceptedCharsets * * @return {String} */ charset (acceptedCharsets) { return nodeReq.charset(this.request, acceptedCharsets) } /** * Returns an array of charsets based upon `Accept-Charset` * header. * * @method charsets * * @return {Array} */ charsets () { return nodeReq.charsets(this.request) } /** * Returns a boolean indicating whether request has * body or not * * @method hasBody * * @return {Boolean} */ hasBody () { return nodeReq.hasBody(this.request) } /** * Returns an object of all the cookies. Make sure always * to define the `secret` inside `config/app.js` file, * since all cookies are signed and encrypted. * * This method will make use of `app.secret` from the config * directory. * * @method cookies * * @return {Object} */ cookies () { return nodeCookie.parse(this.request, this.Config.get(SECRET), true) } /** * Returns cookies without decrypting or unsigning them * * @method plainCookies * * @return {Object} */ plainCookies () { return nodeCookie.parse(this.request) } /** * Returns cookie value for a given key. * * This method will make use of `app.secret` from the config * directory. * * @method cookie * * @param {String} key * @param {Mixed} [defaultValue] * * @return {Mixed} */ cookie (key, defaultValue) { return _.defaultTo(nodeCookie.get(this.request, key, this.Config.get(SECRET), true), defaultValue) } /** * Return raw value for a given key. Cookie will not be * encrypted or unsigned. * * @method plainCookie * * @param {String} key * @param {Mixed} [defaultValue] * * @return {Mixed} */ plainCookie (key, defaultValue) { return _.defaultTo(nodeCookie.get(this.request, key), defaultValue) } /** * Returns a boolean indicating whether request url * matches any of the given route formats. * * @method match * * @param {Array} routes * * @return {Boolean} * * @example * ```js * request.match(['/user/:id', 'user/(+.)']) * ``` */ match (routes) { if (!routes || !routes.length) { return false } const pattern = pathToRegexp(routes, []) return pattern.test(this.url()) } /** * Returns the freshness of a response inside the client cache. * If client cache has the latest response, this method will * return true, otherwise it will return false. * * * Also when HTTP header Cache-Control: no-cache is present this method will return false everytime. * * @method fresh * * @return {Boolean} */ fresh () { return !this._isBadSafari() ? nodeReq.fresh(this.request, this.response) : false } /** * The opposite of {{#crossLink "Request/fresh"}}{{/crossLink}} method. * * @method stale * * @return {Boolean} */ stale () { return !this.fresh() } /** * Returns the request format from the URL params * * @method format * * @return {String} */ format () { const { format } = this.params return format ? (typeof (format) === 'string' ? format.replace(/^\./, '') : format) : null } } /** * Defining _macros and _getters property * for Macroable class * * @type {Object} */ Request._macros = {} Request._getters = {} module.exports = Request