Source: internals.js

/**
 * 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