@adonisjs/framework
Version:
Adonis framework makes it easy for you to write webapps with less code
904 lines (830 loc) • 19.4 kB
JavaScript
'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