/**
* Les méthodes communes à sesatheque-client (index) et fetch,
* à priori réservées à un usage interne, donc sans garantie de perennité
*
* Passer un XMLHttpRequest à setXMLHttpRequest pour une utilisation coté serveur
* @module internals
*/
import xhr from 'sesajstools/http/xhr'
import tools from 'sesajstools'
import { exists, getBaseId, getBaseIdFromRid, getBaseUrl } from './sesatheques'
export { setXMLHttpRequest } from 'sesajstools/http/xhr'
const { hasProp } = tools
const defaultTimeout = 10000
/**
* Liste des tokens éventuels par baseId
* @private
* @type {{}}
*/
const tokens = {}
/**
* Enregistre un token pour une sesatheque (token utilisé par les xhr
* qui en auraient besoin, dans un header `Authorization: sesalabSso xxx`)
* @param baseId
* @param token
*/
export function addSesalabSsoToken (baseId, token) {
if (exists(baseId)) tokens[baseId] = token
else throw Error(`${baseId} n’est pas enregistrée`)
}
/**
* Wrapper de xhr sur l'api, converti une réponse sans message:OK ou une absence de réponse en erreur
* il y aura donc toujours soit une erreur soit une réponse non vide (avec soit message:OK soit des données,
* la propriété data de la réponse)
* Réexporté en fetch par plusieurs autres modules
* @param {string} url
* @param {object} [options] Options passées tel quel à getXhrOptions
* @param {object} [options.method=get] passer post|put|delete si ce n'est pas du get
* @param {object} [options.body] passer un objet pour post|put|delete (ignoré sur get)
* @param {object} [options.isDataExpected=false] passer true pour garantir que next sera appelé avec un objet (sinon ce sera une erreur)
* @param {apiResponseCallback} next (toujours appelée avec une erreur ou en 2nd argument la string OK ou un objet non vide)
* @throws {Error} Si baseId est inconnue
*/
export function callApiUrl (url, options, next) {
if (typeof options === 'function') {
next = options
options = {}
}
// on veut une url absolue ou qui démarre avec /api/
if (!/^(https?:\/\/[a-z0-9\-._]+(:[0-9]+)?\/)api\/./.test(url) && !url.startsWith('/api/')) {
return next(Error(`url ${url} invalide pour appeler l’api`))
}
const opts = getXhrOptions(null, options, url)
// l'analyseur de réponse qui forward
const responseHandler = (error, response) => {
// ajoute un status éventuel à l'erreur retournée
const wrapError = (errorMessage) => {
const error = Error(errorMessage)
if (response && response.status) error.status = response.status
console.error(error)
next(error)
}
if (error) return next(error) // xhr a déjà mis error.status
// check response
if (!response) return wrapError('Le serveur n’a pas renvoyé de réponse')
if (Object.keys(response).length === 0) return wrapError('Le serveur a renvoyé une réponse vide')
if (response.message !== 'OK') {
if (response.message) return wrapError(response.message)
else return wrapError('Le serveur n’a pas renvoyé une réponse malformée')
}
// ça ressemble à une réponse valide
const result = (typeof response.data === 'object' && Object.keys(response.data).length > 0) ? response.data : response.message
if (options.isDataExpected && typeof result === 'string') return wrapError('Le serveur n’a pas renvoyé l’objet attendu')
next(null, result)
}
const method = (options.method && options.method.toLowerCase()) || 'get'
if (['delete', 'post', 'put'].includes(method)) xhr[method](url, options.body, opts, responseHandler)
else xhr.get(url, opts, responseHandler)
}
/**
* Wrapper de xhr.get sur l'api, garanti de renvoyer une erreur ou une réponse non vide
* @param {string} baseId
* @param {string} path chemin qui sera ajouté à /api/, par ex "public/42"
* @param {object} options
* @param {apiResponseCallback} next
*/
export function callApi (baseId, path, options, next) {
if (typeof options === 'function') {
next = options
options = {}
}
try {
const base = getBaseUrl(baseId)
const url = base + 'api/' + path
callApiUrl(url, options, next)
} catch (error) {
next(error)
}
}
/**
* Retourne la base d'une url quelconque
* @param {string} url Une url, avec au moins un caractère après le / racine
* @returns {string|null} null si l'url n'était pas 'conforme'
*/
export function extractBaseUrl (url) {
let base = null
const matches = /^(https?:\/\/[a-z0-9\-._]+(:[0-9]+)?\/)./.exec(url)
if (matches && matches[1]) base = matches[1]
return base
}
/**
* Retourne l'url absolue à passer à callApiUrl pour récupérer les datas
* @param {string} [baseId] facultatif si id est un rid (voire baseId/origine/idOrigine)
* @param {string} id
* @param {{isPublic: boolean, isRef: boolean}} options (props à false par défaut), si isPublic est absent on le déduit de shouldForcePublic(baseId)
* @return {string}
*/
export function getDataUrl (baseId, id, options) {
// gestion du baseId facultatif
if (arguments.length === 1) {
id = baseId
baseId = null
options = {}
} else if (arguments.length === 2) {
if (typeof id === 'string') {
// baseId et id
options = {}
} else {
// rid et options
id = baseId
baseId = null
options = id
}
}
// args ok, id peut être un rid avec baseId null
const isPublic = hasProp(options, 'isPublic') ? options.isPublic : shouldForcePublic(baseId)
const isRef = options.isRef || false
// on regarde si l'id impose sa base
if (id.indexOf('/') !== -1) {
const realBaseId = getBaseIdFromRid(id, false)
if (realBaseId) {
baseId = realBaseId
id = id.substr(baseId.length + 1)
}
}
let url = getBaseUrl(baseId) + 'api/' + (isPublic ? 'public/' : 'ressource/') + id
if (isRef) url += '?format=ref'
return url
}
/**
* Retourne les options pour un appel xhr vers l'api (avec header authorization si on fourni baseId ou url)
* @private
* @param {string} [baseId]
* @param {object} [options] options initiales
* @param {number} [options.timeout=10] timeout en s
* @param {number} [options.public] Si non fourni, sera mis à true si y'a du /public/ dans l'url et false sinon
* @param {string} [url]
* @return {Object} Les options avec ajout de timeout, responseType:json et d'éventuels credentials
*/
export function getXhrOptions (baseId, options, url) {
const opts = Object.assign({ timeout: defaultTimeout }, options)
opts.responseType = 'json'
let isPublic = false
if (url) isPublic = url && /\/public\//.test(url)
else if (options && options.public) isPublic = true
if (!isPublic) {
// faut ajouter des crédentials
if (!baseId && url) {
// on déduit baseId de l'url
baseId = getBaseId(extractBaseUrl(url))
}
// si on a un token on le prend
if (baseId) {
if (tokens[baseId]) {
if (!opts.headers) opts.headers = {}
opts.headers.authorization = 'sesalabSso ' + tokens[baseId]
} else if (isMyBaseId(baseId)) {
// fallback sur les cookies de session seulement en same-domain
opts.withCredentials = true
} else {
// fallait filer un token avant (hors du site courant on doit toujours avoir un token),
// en cross-domain les cookies passent pas en sécurité haute sous safari, ou en navigation privée chez les autres,
// et y'aurait un risque d'avoir des cookies d'une autre session précédente, d'un user ≠ de celui du token
console.error(Error(`appel de la sesatheque ${baseId} sans token dispo (${url})`))
}
} else {
console.error(Error(`BaseId de la sesatheque inconnue à ce stade, impossible d'ajouter un token`))
}
}
// console.log('xhrDefaultOptions pour ' + baseId + (url ? ' et ' + url : ''), opts)
return opts
}
/**
* Renvoie true si on a un token pour cette sesatheque
* @param {string} baseId
* @returns {boolean}
*/
export function hasToken (baseId) {
return Boolean(tokens[baseId])
}
/**
* Retourne true si on est dans un navigateur et que baseId correspond au domaine courant, undefined si pas dans un navigateur
* @param baseId
* @return {boolean|undefined}
*/
export function isMyBaseId (baseId) {
if (typeof window !== 'undefined') {
return baseId === getBaseId(`${window.location.protocol}//${window.location.host}/`, null)
}
}
/**
* Retourne true si on a ni token ni session sur un same-domain dans un navigateur
* @param {string} baseId
* @return {boolean}
*/
export function shouldForcePublic (baseId) {
if (hasToken(baseId)) return false
return !(isMyBaseId(baseId) && typeof window !== 'undefined' && window.document.cookie)
}
// pas d'export par défaut