'use strict'
/*
* @preserve This file is part of "lassi".
* Copyright 2009-2014, arNuméral
* Author : Yoran Brault
* eMail : yoran.brault@arnumeral.fr
* Site : http://arnumeral.fr
*
* "lassi" is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* "lassi" is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with "lassi"; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
var _ = require('lodash')
/**
* Contexte d'exécution d'une action.
* @param {Request} request la requête Express
* @param {Response} response la réponse Express
* @fires Lassi#context
* @constructor
*/
class Context {
constructor (request, response) {
/**
* La requête Express
* @see http://expressjs.com/api.html#request
*/
this.request = request
/**
* La réponse Express
* @see http://expressjs.com/api.html#response
*/
this.response = response
/**
* Alias de response.cookie
* @see http://expressjs.com/en/4x/api.html#res.cookie
* @type function
* @param {string} name Nom du cookie
* @param {string} value Sa valeur
* @param {object} [options]
* @param {string} [options.domain]
* @param {function} [options.encode=encodeURIComponent] Callback synchrone d'encodage de la valeur du cookie
* @param {Date} [options.expires]
* @param {boolean} [options.httpOnly]
* @param {number} [options.maxAge] en s (depuis cette réponse)
* @param {string} [options.path]
* @param {boolean} [options.secure]
* @param {boolean} [options.signed]
* @param {boolean} [options.sameSite]
*/
this.setCookie = this.response.cookie
/** La méthode http utilisée (en minuscules) */
this.method = request.method.toLowerCase()
/**
* Les paramètres passés en get, alias vers request.query
* @see http://expressjs.com/api.html#req.query
*/
this.get = this.request.query
/**
* Les paramètres passés en post, alias vers request.body
* http://expressjs.com/api.html#req.body
*/
this.post = this.request.body
/** La session */
this.session = this.request.session || {}
/**
* Évènement généré de la création d'un nouveau contexte.
* @param {Context} context le context fraîchement créé.
* @event Lassi#context
*/
lassi.emit('context', this)
} // constructor
/**
* Retourne la valeur du cookie (qui était dans la requête), lit simplement request.cookies
* @see http://expressjs.com/en/4x/api.html#req.cookies
* @param {string} name
* @return {string|undefined} undefined si le cookie n'existe pas, chaine vide si c'est sa valeur
*/
getCookie (name) {
return this.request.cookies && this.request.cookies[name]
}
/**
* Détermine si la requête comporte des arguments Get
* @return {Boolean} vrai si c'est le cas.
*/
hasGet () { return !_.isEmpty(this.get) }
/**
* Détermine si la requête comporte des arguments Post
* @return {Boolean} vrai si c'est le cas.
*/
hasPost () { return !_.isEmpty(this.post) }
/**
* Détermine si la requête est de type Get
* @return {Boolean} vrai si c'est le cas.
*/
isGet () { return this.method === 'get' }
/**
* Détermine si la requête est de type Post
* @return {Boolean} vrai si c'est le cas.
*/
isPost () { return this.method === 'post' }
/**
* Provoque une redirection.
*
* @param {String} path Le chemin de la redirection
* @param {Integer=} [code=302] Le code de redirection à utiliser (301 pour une redirection permanente)
* @param {boolean} [disableAutoCacheControl] Passer true pour que cette fct n'ajoute pas de header cache-control (sinon elle ajoute du noCache sur les code≠301)
*/
redirect (path, code, disableAutoCacheControl) {
this.location = path
this.status = code || 302
if (this.status !== 301 && !disableAutoCacheControl) this.setNoCache()
this.next()
}
/**
* Provoque la génération d'un access denied (403)
* @param {string=} message Le message éventuel (sera "access denied" si non fourni)
*/
accessDenied (message) {
this.status = 403
this.plain(message || 'access denied')
}
/**
* Provoque la génération d'un not found (404)
*
* @param {string=} message Le message éventuel (sera "not found" si non fourni)
*/
notFound (message) {
this.status = 404
this.plain(message || 'not found')
}
/**
* Renvoie une réponse de type JSON.
* @param {object} data Les données (attention à renvoyer une liste de propriétés,
* un array est transformé en objet, un string devient la propriété content
* et les autres types primitifs sont ignorés)
*/
json (data) {
this.contentType = 'application/json'
this.next(null, data)
}
/**
* Renvoie une réponse de type jsonP (du code js)
* Appelle la fonction précisée dans context.$callbackName, ou passée en get avec ?callback=...
* ou à défaut "callback"
* @param {object} data Les données à passer en paramètre à la fct de callback,
* attention à ne pas envoyer de références circulaires
*/
jsonP (data) {
this.contentType = 'application/javascript'
// on formate le code js
var callbackName = this.$callbackName || this.get.callback || 'callback'
var jsString = callbackName + '('
// stringify peut planter en cas de références circulaire (faudra cloner avant)
try {
jsString += JSON.stringify(data)
} catch (error) {
jsString += '{ "error" : "' + error.toString().replace('"', '\\"') + '"}'
}
jsString += ');'
this.next(null, jsString)
}
/**
* Renvoie une réponse de type HTML.
* @param {object} data données
*/
html (data) {
this.contentType = 'text/html'
this.next(null, data)
}
/**
* Renvoie une réponse en text/plain
* @param {string} text
*/
plain (text) {
this.contentType = 'text/plain'
this.next(null, { content: text })
}
/**
* Renvoie une réponse raw
* @param {string} text
*/
raw (content, options) {
if (options.attachment) this.response.attachment(options.attachment)
if (options.headers) {
for (var prop in options.headers) {
this.response.append(prop, options.headers[prop])
if (prop === 'Content-Type') this.contentType = options.headers[prop]
}
}
// on force le transport
this.transport = 'raw'
this.next(null, { content: content })
}
/**
* Renvoie en json un message d'erreur attaché à un champ
* L'objet envoyé sera de la forme {field: 'le nom du champ passé (ou absent)', message: 'le message', success: false}
* @param {string|null} field le nom du champ en erreur
* @param message Le message d'erreur
*/
fieldError (field, message) {
var data = {}
if (field) data.field = field
data.message = message
data.success = false
this.json(data)
}
/**
* Envoi data (cloné) en json en ajoutant une propriété success mise à true
* @param data Les données à envoyer en json
*/
rest (data) {
data = _.clone(data)
data.success = true
this.json(data)
}
/**
* envoie data en json, avec toujours succes:false et message (ajouté si non fourni)
* Si data est une Error, ça l'écrira en console.error (en ne passant que le message en json, sans la stack)
* Pour éviter ce console.error, il suffit donc d'appeler cette fonction avec error.message plutôt que error
* Cette méthode ne gère pas de niveau d'erreur, pour le faire l'appelant devra passer un objet {message, level}
* (ou {message, errorLevel} ou {message, priority} ou ce qui lui plaît davantage)
* @param {String|Error|Object} data
*/
restKo (data) {
let response
if (typeof data === 'string') {
response = {
message: data
}
} else if (data instanceof Error) {
console.error(data)
response = {
message: data.message
}
} else if (typeof data === 'object') {
response = _.clone(data)
if (!response.message) response.message = 'erreur inconnue'
} else {
console.error(new Error('restKO appelé avec un argument invalide'), data)
response = {
message: 'erreur inconnue'
}
}
response.success = false
this.json(response)
}
/**
* Ajoute un header à la réponse
* @param name
* @param value
*/
setHeader (name, value) {
this.response.setHeader(name, value)
}
/**
* Ajoute un header Cache-Control pour empêcher la mise en cache de la réponse
*/
setNoCache () {
this.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
}
/**
* Ajoute un header pour mise en cache (par un proxy ou le navigateur)
* @param {string} [maxAge=1d] Sans unité c'est des secondes
*/
setPublicCache (maxAge = '1d') {
this.setHeader('Cache-Control', `public, max-age=${maxAge}`)
}
/**
* Fixe le status (code http de la réponse)
* @param {number} status
*/
setStatus (status) {
if (typeof status !== 'number' || status < 100 || status > 599) return console.error(new Error('invalid status'))
this.status = status
}
/**
* Retourne un header de la requete
* @param {string} name Le header que l'on cherche
* @param {string} [defaultValue=undefined] Valeur à retourner si le header name n'existe pas
* @returns {string|undefined}
*/
header (name, defaultValue) {
if (_.has(this.request.headers, name)) {
return this.request.headers[name]
} else {
return defaultValue
}
}
}
module.exports = Context
/**
* Booléen qui peut être mis par un contrôleur pour court-circuiter les contrôleurs suivants.
* À priori, ça devrait plutôt être fait en passant une erreur à context.next(), ou en la mettant dans context.error,
* mais on laisse cette possibilité.
* @name skipNext
* @memberOf Context
* @default undefined
* @type {boolean}
*/
/**
* Erreur qui peut être mise par un contrôleur, mais à priori affectée lors d'un appel de context.next(error)
* Sa présence court-circuite les contrôleurs suivants.
* Utile par exemple pour un listener beforeTransport (par ex pour ajouter status ou content-type si ça manque)
* @name error
* @memberOf Context
* @default undefined
* @type {Error}
*/
/**
* À priori affecté après le passage de tous les contrôleurs, d'après le content-type, mais un contrôleur
* qui veut traiter lui-même la réponse via context.response (l'objet response d'express) peut le faire en
* affectant `context.transport = 'done'` pour indiquer à lassi de ne plus s'occuper de la réponse.
* @name transport
* @memberOf Context
* @default undefined
* @type {string}
*/
/**
* Méthode pour passer à l'action suivante (affecté et géré par le middleware de Controllers)
* Un contrôleur appelle en général `context.next(error, data)` lorsqu'il a fini.
* Si error, lassi gère (court-circuite les contrôleurs suivant et envoie la réponse, qui sera par défaut une erreur 500 en plain/text).
* Sans erreur, les data renvoyés par tous les contrôleurs sont mergés et lassi envoie la réponse.
* @name next
* @memberOf Context
* @type {function}
*/