/**
* This file is part of Sesatheque.
* Copyright 2014-2015, Association Sésamath
*
* Sesatheque is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation.
*
* Sesatheque 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Sesatheque (LICENCE.txt).
* @see http://www.gnu.org/licenses/agpl.txt
*
*
* Ce fichier fait partie de l'application Sésathèque, créée par l'association Sésamath.
*
* Sésathèque est un logiciel libre ; vous pouvez le redistribuer ou le modifier suivant
* les termes de la GNU Affero General Public License version 3 telle que publiée par la
* Free Software Foundation.
* Sésathèque est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE,
* sans même la garantie tacite de QUALITÉ MARCHANDE ou d'ADÉQUATION à UN BUT PARTICULIER.
* Consultez la GNU Affero General Public License pour plus de détails.
* Vous devez avoir reçu une copie de la GNU General Public License en même temps que Sésathèque
* (cf LICENCE.txt et http://vvlibri.org/fr/Analyse/gnu-affero-general-public-license-v3-analyse
* pour une explication en français)
*/
'use strict'
var hasProp = require('../index').hasProp
/* eslint-env browser */
/**
* Pour gérer les appels ajax
* @module sesajstools/http/xhr
*/
/**
* 10s de timeout par défaut mis à toutes les requêtes qui n'en précisent pas
* @private
* @type {Integer}
*/
var defaultTimeout = 10000
var minTimeout = 100
var maxTimeout = 60000
var MyXMLHttpRequest
/**
* Retourne responseText si ça existe dans ce xmlHttpRequest (sinon chaîne vide)
* @param {XMLHttpRequest} xmlHttpRequest
* @return {string}
*/
function getResponseText (xmlHttpRequest) {
try {
return (xmlHttpRequest && hasProp(xmlHttpRequest, 'responseText') && xmlHttpRequest.responseText) || ''
} catch (error) {
// sur un xhr standard la propriété responseText n'est pas accessible si responseType vaut json, mais
return ''
}
}
/**
* Fonction qui gère l'appel xhr
* @private
* @param {string} verb
* @param {string} url
* @param {object} data
* @param {xhrOptions} options
* @param {function} callback Sera appelée avec (error, réponse),
* si options.responseType === 'json' la réponse sera un objet,
* sinon l'objet response du XMLHttpRequest
*/
function xhrCall (verb, url, data, options, callback) {
// pour s'assurer qu'on ne l'appelle qu'une fois, entre timeout, error et done
function next (error, data) {
if (!isNextCalled) {
isNextCalled = true
callback(error, data)
}
}
if (typeof options === 'function') {
callback = options
options = {}
}
if (typeof callback !== 'function') throw new Error('xhr need a callback')
if (['DELETE', 'GET', 'POST', 'PUT'].indexOf(verb) === -1) return callback(new Error('invalid verb'))
var xhr
var isNextCalled = false
if (!options || typeof options !== 'object') options = {}
if (MyXMLHttpRequest) {
xhr = new MyXMLHttpRequest()
} else if (typeof XMLHttpRequest !== 'undefined') {
xhr = new XMLHttpRequest() // eslint-disable-line no-undef
}
if (!xhr) return next(new Error('Votre navigateur ne permet pas de faire des appels ajax'))
if (options.urlParams) {
for (var p in options.urlParams) {
if (hasProp(options.urlParams, p)) {
url += (url.indexOf('?') > 0) ? '&' : '?'
url += p + '='
url += encodeURIComponent(options.urlParams[p])
}
}
}
xhr.open(verb, url, !options.sync)
if (options.withCredentials) xhr.withCredentials = true
if (options.responseType) xhr.responseType = options.responseType
// mettre un Content-Type sur du GET déclenche un preflight,
// Ce Content-Type ne concerne que ce que l'on envoie donc on regarde
// si y'a un objet à envoyer et que Content-Type n'est pas précisé on ajoute json
if (!!data && typeof data === 'object' && (!options.headers || !options.headers['Content-Type'])) {
xhr.setRequestHeader('Content-Type', 'application/json')
}
// sinon l'appelant devra préciser ce qu'il veut (on peut poster du xml par ex),
if (options.headers) {
for (var header in options.headers) {
if (hasProp(options.headers, header) && typeof options.headers[header] === 'string') {
xhr.setRequestHeader(header, options.headers[header])
}
}
}
// pas de timeout si sync
if (options.sync) {
console.warn('Appel xhr synchrone deprecated (chrome l’ignore), au unload préférer navigator.sendBeacon si c’est dispo (il faut faire de l’async dans tous les autres cas)')
} else {
xhr.timeout = options.timeout || defaultTimeout
if (xhr.timeout < minTimeout) {
console.error(new Error('timeout ' + xhr.timeout + "ms trop faible sur l'url " + url + ' réinitialisé à ' + (defaultTimeout / 1000) + 's'))
xhr.timeout = defaultTimeout
}
if (xhr.timeout > maxTimeout) {
console.error(new Error('timeout ' + xhr.timeout + "ms trop élevé sur l'url " + url + ' réinitialisé à ' + (defaultTimeout / 1000) + 's'))
xhr.timeout = defaultTimeout
}
}
xhr.onerror = options.onerror || function () {
// Pb de connexion au serveur
var errMsg = 'Le serveur a renvoyé une erreur'
if (xhr.status) errMsg += ' ' + xhr.status
var responseText = getResponseText(xhr)
if (responseText) errMsg += ' : ' + responseText
next(new Error(errMsg))
}
xhr.ontimeout = function () {
var errMsg = "Le serveur n'a pas répondu"
if (options.timeout) errMsg += ' après ' + Math.floor(options.timeout / 1000) + "s d'attente."
var error = new Error(errMsg)
error.isTimeout = true
next(error)
}
xhr.onreadystatechange = function () {
function parse (str) {
try {
return JSON.parse(str)
} catch (err) {
error = err
}
}
var error, retour
if (this.readyState === this.DONE) {
// et IE11 renvoie une string dans this.response sans responseType
var jsonRe = /^application\/json/i
var isJson = xhr.responseType === 'json' || jsonRe.test(xhr.getResponseHeader('Content-Type'))
// avec firefox, on a parfois des réponses 304 du serveur
// (avec If-None-Match $etag à l'aller et 304 au retour)
// qui donnent ici un status 0 avec une réponse, si y'a une réponse avec success, true ou false, ou message ok)
if (
this.status === 200 ||
(this.response && hasProp(this.response, 'success')) ||
(this.response && this.response.message === 'ok')
) {
if (this.response) {
// ie11 n'affecte pas responseType à 'json' et renvoie une string, d'où la regex sur le Content-Type
// on se charge de transformer la réponse en objet
if (typeof this.response === 'string' && isJson) {
retour = parse(this.response)
} else {
retour = this.response
}
} else {
// un navigateur gère ça tout seul, avec this.response en objet si y'avait un content-type json
// mais le module xmlhttprequest ne renvoie que responseXml et responseText (avec responseType json)
var responseText = getResponseText(this)
if (isJson && responseText) {
// pour xmlhttprequest seulement, car avec un navigateur on passe jamais là
retour = parse(responseText)
} else {
retour = responseText
}
}
// on précise si ≠ 200 (à priori 304, les autres cas passent en erreur dans le else qui suit)
if (this.status !== 200) retour.status = this.status
} else {
// KO (les redirections sont normalement gérées par le navigateur)
var message
switch (this.status) {
case 0:
message = 'Pas de connexion'
break
case 400:
message = 'Requête invalide'
break
case 401:
message = 'Authentification requise'
break
case 403:
message = 'Accès refusé'
break
case 404:
message = 'Url inexistante'
break
case 500:
message = 'Erreur serveur'
break
case 503:
message = 'Serveur indisponible'
break
default:
message = 'Erreur ' + this.status
}
message += ' sur ' + verb + ' ' + url
// au cas où c'est du json qui renvoie une erreur
var addOn
if (this.response) {
if (typeof this.response === 'string' && isJson) {
retour = parse(this.response)
addOn = retour.error || retour.message
retour = undefined
} else {
addOn = this.response.error || this.response.message
}
if (addOn) message += ' qui précise « ' + addOn + ' »'
}
error = new Error(message)
error.status = this.status
error.content = this.response
}
next(error, retour)
}
}
if (data) {
try {
data = JSON.stringify(data)
} catch (error) {
console.error(error)
data = undefined
}
}
xhr.send(data)
} // xhrCall
module.exports = {
/**
* Appel ajax en DELETE
* Si 3 arguments, le 2e sera pris comme options
* @param {string} url
* @param {string|object} [data] Données éventuelles à envoyer dans le body de la requête
* @param {xhrOptions} [options]
* @param {responseCallback} callback
*/
delete: function del (url, data, options, callback) {
// dernier argument doit être callback, on vérifie pas les types,
// c'est xhrCall qui se plaindra si c'est pas une fct
if (arguments.length === 3) {
callback = options
options = data
data = undefined
} else if (arguments.length === 2) {
callback = data
data = undefined
options = {}
}
xhrCall('DELETE', url, data, options, callback)
},
/**
* Appel ajax en GET
* @param {string} url
* @param {xhrOptions} [options]
* @param {responseCallback} callback
*/
get: function get (url, options, callback) {
xhrCall('GET', url, null, options, callback)
},
/**
* Appel ajax en POST
* @param {string} url
* @param {object} data Données à poster dans le body
* @param {xhrOptions} [options] json sera automatiquement ajouté si data est un object,
* sinon faut préciser Content-type
* @param {responseCallback} callback
*/
post: function post (url, data, options, callback) {
xhrCall('POST', url, data, options, callback)
},
/**
* Appel ajax en PUT
* @param {string} url
* @param {object} data Données éventuelles à envoyer dans le body de la requête
* @param {xhrOptions} [options]
* @param {responseCallback} callback
*/
put: function put (url, data, options, callback) {
xhrCall('PUT', url, data, options, callback)
},
/**
* Affecte un XMLHttpRequest (pour utiliser ce module coté serveur)
* par ex `xhr.setXMLHttpRequest(require('xmlhttprequest').XMLHttpRequest)
* @param {XMLHttpRequest} XMLHttpRequest
*/
setXMLHttpRequest: function (XMLHttpRequest) {
MyXMLHttpRequest = XMLHttpRequest
}
}
/**
* Options éventuelles à passer avec la requête xhr
* @typedef xhrOptions
* @param {string} responseType Préciser json pour récupérer un objet plutôt qu'une string dans la réponse
* @param {object} headers Liste de headers à ajouter à l'appel, sous la forme header:headerValue (par ex {'Content-Type':'text/xml'})
* @param {boolean} withCredentials Passer true pour l'ajouter
* @param {object} urlParams Liste de clé:valeur à ajouter à l'url (les valeurs seront passées à encodeURIComponent)
* @param {boolean} sync Passer true pour passer en appel ajax synchrone (donc pas de timeout possible,
* à éviter mais indispensable si c'est sur un evt unload, justement pour le mode bloquant)
*/
/**
* @callback responseCallback
* @param {Error} error Erreur éventuelle
* @param {string|object} response L'objet response du XMLHttpRequest, un objet si on avait précisé options.responseType = 'json', une string sinon
*/