'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.
*/
const _ = require('lodash')
const log = require('an-log')('lassi-actions')
const { hasProp } = require('sesajstools')
const constantes = require('./constantes')
// sera initialisé d'après les settings au 1er appel d'un contrôleur
let maxTimeout
/**
* Fonction fauchée ici : http://forbeslindesay.github.io/express-route-tester/
* car le module https://github.com/component/path-to-regexp marche finalement
* moins bien...
*/
function pathtoRegexp (path, keys, options) {
options = options || {}
const sensitive = options.sensitive
const strict = options.strict
keys = keys || []
if (path instanceof RegExp) return path
if (path instanceof Array) path = '(' + path.join('|') + ')'
path = path
.concat(strict ? '' : '/?')
.replace(/\/\(/g, '(?:/')
.replace(/\+/g, '__plus__')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function (_, slash, format, key, capture, optional) {
keys.push({ name: key, optional: !!optional })
slash = slash || ''
return '' +
(optional ? '' : slash) +
'(?:' +
(optional ? slash : '') +
(format || '') + (capture || ((format && '([^/.]+?)') || '([^/]+?)')) + ')' +
(optional || '')
})
.replace(/([/.])/g, '\\$1')
.replace(/__plus__/g, '(.+)')
.replace(/\*/g, '(.*)')
return new RegExp('^' + path + '$', sensitive ? '' : 'i')
}
/**
* Callback d'une action.
* @callback Action~callback
* @param {Context} context
*/
/**
* Constructeur de l'action.
* @param {string} path le chemin (ou la partie de chemin) associée à l'action
* @constructor
* @private
*/
class Action {
constructor (controller, methods, path, cb) {
if (!_.isFunction(cb)) {
if (!_.isObject(cb)) throw new Error('invalid cb passed to Action constructor')
// c'est un objet, on l'ajoute
Object.assign(this, cb)
this.callback = undefined
this.middleware = true
} else {
this.callback = cb
this.middleware = undefined
}
this.path = path
this.methods = methods
if (this.path && this.path.trim() === '') this.path = undefined
if (typeof this.path !== 'string') {
this.path = controller.path
} else if (this.path.charAt(0) !== '/') {
this.path = controller.path + '/' + this.path
}
this.path = this.path.replace(/\/+/, '/')
if (this.path !== '/') {
this.path = this.path.replace(/\/+$/, '')
}
if (this.middleware) {
const express = require('express')
const options = {}
if (lassi.settings && lassi.settings.pathProperties && lassi.settings.pathProperties[this.path]) {
Object.assign(options, lassi.settings.pathProperties[this.path])
}
// par défaut, express met un max-age à 0 (cf http://expressjs.com/en/4x/api.html#express.static)
// si l'appli ne précise rien on le met à 1h sur le statique
if (!hasProp(options, 'maxAge')) options.maxAge = '1h'
const serveStatic = express.static(this.fsPath, options)
this.middleware = (function (base) {
return function (request, response, next) {
const saveUrl = request.url
request.url = request.url.substr(base.length)
if (request.url.length === 0 || request.url.charAt(0) !== '/') request.url = '/' + request.url
serveStatic(request, response, function () {
request.url = saveUrl
next()
})
}
})(this.path)
this.path += '*'
}
this.pathRegexp = pathtoRegexp(this.path, this.keys = [], { sensitive: true, strict: true, end: false })
log('Add route',
(this.methods ? this.methods.join(',') : 'ALL').toUpperCase(),
this.path.yellow,
this.pathRegexp,
this.middleware ? ' -> ' + cb : ''
)
}
/**
* Vérifie si une route est gérée par le contrôleur
* @param path La route à tester
* @returns {Object} Les paramètres de la route qui correspondent au pattern du contrôleur
*/
match (method, path) {
const params = {}
let key
let val
method = method.toLowerCase()
if (this.methods && !this.methods.includes(method)) return null
const match = this.pathRegexp.exec(path)
// console.log(path, this.pathRegexp, match);
if (!match) return null
let paramIndex = 0
const len = match.length
for (let i = 1; i < len; ++i) {
key = this.keys[i - 1]
try {
val = typeof match[i] === 'string' ? decodeURIComponent(match[i]) : match[i]
} catch (e) {
const err = new Error("Failed to decode param '" + match[i] + "'")
err.status = 400
throw err
}
if (key) {
params[key.name] = val
} else {
params[paramIndex++] = val
}
}
return params
}
/**
* Lance l'exécution de la pile de callbacks
* @param {Context} context
* @param {Function} next
*/
execute (context, next) {
let timer = false
let isCbCompleted = false
function fooProtect () {
console.error(new Error('Attention, un résultat est arrivé de manière inattendue (un appel de next en trop ?).'))
}
function processResult (error, result) {
context.next = fooProtect
isCbCompleted = true
if (timer) clearTimeout(timer)
if (typeof result === 'undefined' && !(error instanceof Error)) {
result = error
error = null
}
next(error, result)
}
try {
// on ne peut pas appeler $settings ou lassi.service en dehors de la classe car ce fichier est requis
// avant bootstrap, on initialise donc maxTimeout lors du 1er appel d'un controleur après le boot
if (!maxTimeout) {
const $settings = lassi.service('$settings')
maxTimeout = $settings.get('$server.maxTimeout', constantes.maxTimeout - constantes.minDiffTimeout)
}
context.next = processResult
this.callback.call(context, context)
// Timeout de 1s par défaut après le retour synchrone
// (ça permet aussi à l'action de modifier son timeout pendant son exécution)
let timeout = context.timeout || this.callback.timeout || constantes.defaultTimeout
if (timeout > maxTimeout) {
console.error(new Error(`timeout ${timeout} supérieur au maximum autorisé dans cette application ${maxTimeout}, il sera ramené à ${maxTimeout - constantes.minDiffTimeout}`))
// pour laisser le timeout ci-dessous prendre la main sur celui de node
timeout = maxTimeout - constantes.minDiffTimeout
}
// Si aucune donnée synchrone n'est déjà reçue, on arme le timeout
if (!isCbCompleted) {
timer = setTimeout(function () {
timer = false
next(new Error('Timeout while executing (' + timeout + 'ms)'))
}, timeout)
}
} catch (e) {
processResult(e)
}
}
}
module.exports = Action