Home Reference Source

src/URLHelper.js

import $ from 'jquery'
import { Debug } from './Debug'

/**
 * @typedef {object} URLConfig
 * @property {Localizable} url
 * @property {boolean} [useProxy]
 * @property {string} [proxy]
 * @property {boolean} [cache=true]
 * @property {string} [username] only implemented for wms at the moment
 * @property {string} [password] only implemented for wms at the moment
 */

/**
 * @typedef {URLConfig|Localizable|URL} URLLike
 */

export class URL {
  /**
   * @param {URLLike} urlLike
   * @param {G4UMap|null} map
   */
  constructor (urlLike, map) {
    if ($.type(urlLike) === 'string' || !urlLike.hasOwnProperty('url')) {
      /**
       * @type {string}
       */
      this.url = urlLike
      /**
       * @type {boolean}
       */
      this.useProxy = false
      /**
       * @type {boolean}
       */
      this.cache = true
      this.params = []
      this.expand = []
    } else {
      /**
       * @type {Localizable}
       */
      this.url = urlLike.url
      /**
       * @type {boolean}
       */
      this.useProxy = urlLike.useProxy
      /**
       * @type {string}
       */
      this.proxy = urlLike.proxy
      /**
       * @type {boolean}
       */
      this.cache = urlLike.cache === undefined ? true : urlLike.cache
      /**
       * @type {string}
       */
      this.username = urlLike.username
      /**
       * @type {string}
       */
      this.password = urlLike.password
      if (urlLike.params) {
        this.params = urlLike.params.slice(0)
      } else {
        this.params = []
      }
      if (urlLike.expand) {
        this.expand = urlLike.expand.slice(0)
      } else {
        this.expand = []
      }

      /**
       * @type {string}
       */
      this.globalProxy = urlLike.globalProxy
      /**
       * {L10N}
       */
      this.localiser = urlLike.localiser
    }

    if (map) {
      this.extractParamsFromMap(map)
    }
  }

  /**
   * @param {G4UMap} map
   */
  extractParamsFromMap (map) {
    /**
     * @type {string}
     */
    this.globalProxy = this.globalProxy || map.get('proxy')
    /**
     * {L10N}
     */
    this.localiser = this.localiser || map.get('localiser')
  }

  /**
   * @param {object} config
   * @param {string} paramName
   * @param {string} [defaultValue]
   * @param {G4UMap} [map]
   * @returns {URL}
   */
  static extractFromConfig (config, paramName, defaultValue, map) {
    if (!config.hasOwnProperty(paramName)) {
      return null
    }
    if (config.hasOwnProperty('useProxy') || config.hasOwnProperty('proxy') || config.hasOwnProperty('cache')) {
      Debug.warn('Using the parameters \'useProxy\' \'proxy\' \'cache\' directly inside a config object is considered' +
        ' deprecated. Please use a URLConfig object')
      return new URL({
        url: config[paramName] || defaultValue,
        useProxy: config.useProxy,
        proxy: config.proxy,
        cache: config.cache
      }, map)
    } else if ($.isPlainObject(config[paramName]) && !config[paramName].hasOwnProperty('url')) {
      Debug.warn('The url config object is missing an "url" parameter. The software is assuming the' +
        ' parameter is a Localizable.')
      return new URL({
        url: config[paramName]
      }, map)
    } else {
      return new URL(config[paramName], map)
    }
  }

  /**
   * @returns {URL}
   */
  clone () {
    return new URL(this)
  }

  /**
   * @returns {string}
   */
  finalize () {
    let url = this
    if (!this.cache) {
      url = this.clone().addParam(Math.random().toString(36).substring(7))
    }

    let urlAsString = this.localiser ? this.localiser.selectL10N(url.url) : url.url

    if (url.params.length) {
      if (urlAsString.search(/\?/) === -1) {
        urlAsString += '?'
      } else {
        urlAsString += '&'
      }
      urlAsString += url.params.join('&')
    }

    for (const expand of url.expand) {
      urlAsString = URL.expandTemplate_(urlAsString, expand)
    }

    if (url.useProxy === true || (url.useProxy === undefined && !!url.proxy)) {
      const proxy = url.proxy || this.globalProxy
      if (!proxy) {
        throw new Error('No proxy configured. Either configure a local or global proxy if you want to use the option' +
          ' useProxy.')
      }

      return URL.expandTemplate_(proxy,
        { paramName: 'url', paramValue: URL.encodeTemplate_(urlAsString), required: true })
    } else {
      return urlAsString
    }
  }

  /**
   * this function will add an parameter to the url
   * @param {string} param
   * @returns {URL}
   */
  addParam (param) {
    this.params.push(param)
    return this
  }

  /**
   * replaces a param enclosed in {} in a (url) template with a value. If the value is an array it will take any string
   * not containing a '}' after the paramName to join the array, default ','.
   * @param {string} template an (url) template
   * @param {object} expand
   * @param {string} expand.paramName the parameter that will be replaced (given without {}) f.e. 'example' will
   *    replace any occurancy of '{example}' (after the word 'example' there might be given a string join an
   *    array value i.e. '{example+}')
   * @param {string|string[]|number} expand.paramValue the value(s) which will be inserted
   * @param {boolean} expand.required
   * @returns {string} the expanded string
   */
  static expandTemplate_ (template, expand) {
    const regexp = new RegExp('{' + expand.paramName + '([^}]*)}')
    const match = template.match(regexp)
    if (match) {
      if ($.type(expand.paramValue) === 'string') {
        return template.replace(regexp, expand.paramValue)
      } else if ($.type(expand.paramValue) === 'array') {
        const joinString = match[1] || ','
        return template.replace(regexp, expand.paramValue.join(joinString))
      } else if ($.type(expand.paramValue) === 'number') {
        const valReg = new RegExp('[:,]([^,])', 'g')
        let nextMatch = valReg.exec(match[1])
        for (let i = 0; i < expand.paramValue; i++) {
          nextMatch = valReg.exec(match[1])
        }
        return template.replace(regexp, nextMatch[1])
      }
    } else if (expand.required) {
      throw new Error('required parameter ' + expand.paramName + ' (enclosed in {}) not found in string ' + template)
    } else {
      return template
    }
  }

  /**
   * expand the target template. automatically encodes the value
   * @param {string} paramName
   * @param paramValue
   * @param {boolean} [required=true]
   * @returns {URL}
   */
  expandTemplate (paramName, paramValue, required = true) {
    const index = this.expand.findIndex(e => e.paramName === paramName)
    if (index >= 0) {
      this.expand.splice(index, 1)
    }
    const encode = val => {
      if ($.type(val) === 'string') {
        return encodeURIComponent(val)
      } else if ($.type(paramValue) === 'array') {
        return val.map(v => encode(v))
      } else {
        return val
      }
    }
    paramValue = encode(paramValue)
    this.expand.push({ paramName, paramValue, required })
    return this
  }

  /**
   * @param {string} otherUrl
   * @returns {URL}
   */
  useProxyFor (otherUrl) {
    return new URL({
      useProxy: this.useProxy,
      proxy: this.proxy,
      url: otherUrl,
      localiser: this.localiser,
      globalProxy: this.globalProxy
    })
  }

  /**
   * this function takes an (url) template and encodes everything except for the templated elements.
   * @param {string} template an (url) template
   * @returns {string} the encoded (url) template
   */
  static encodeTemplate_ (template) {
    const parts = template.split('}')

    let encodedTemplate = ''

    let i
    for (i = 0; i < parts.length - 1; i += 1) {
      const partedParts = parts[i].split('{')
      encodedTemplate += encodeURIComponent(partedParts[0]) + '{' + partedParts[1] + '}'
    }

    encodedTemplate += encodeURIComponent(parts[i])

    return encodedTemplate
  }

  /**
   * this method returns a new URL extend by the provided string
   * @param {string} extension
   * @returns {URL}
   */
  extend (extension) {
    const newUrl = this.clone()
    newUrl.url += extension
    return newUrl
  }

  setAuth (xhr) {
    if (this.username && this.password) {
      const auth = window.btoa(`${this.username}:${this.password}`)
      xhr.withCredentials = true
      xhr.setRequestHeader(this.useProxy ? 'X-Proxy-Forward-Authorization' : 'Authorization', 'Basic ' + auth)
    }
  }
}