Source: Core/CorsProxy.js

"use strict";

/*global require*/
var URI = require('urijs');

var defined = require('terriajs-cesium/Source/Core/defined');
var defaultValue = require('terriajs-cesium/Source/Core/defaultValue');
var loadJson = require('./loadJson');
var FeatureDetection = require('terriajs-cesium/Source/Core/FeatureDetection');

var DEFAULT_BASE_PROXY_PATH = 'proxy/';

/**
 * Rewrites URLs so that they're resolved via the TerriaJS-Server proxy rather than going direct. This is most useful
 * for getting around CORS restrictions on services that don't have CORS set up or when using pre-CORS browsers like IE9.
 * Going via the proxy is also useful if you want to change the caching headers on map requests (for instance many map
 * tile providers set cache headers to no-cache even for maps that rarely change, resulting in a much slower experience
 * particularly on time-series data).
 *
 * @param overrideLoadJson A method for getting JSON from a URL that matches the signature of Core/loadJson
 *      module - this is overridable mainly for testing.
 * @constructor
 */
function CorsProxy(overrideLoadJson) {
    this.loadJson = defaultValue(overrideLoadJson, loadJson);

    // Note that many of the following are intended to be set by a request to the server performed in {@link CorsProxy#init},
    // but these can be overridden if necessary.

    /**
     * The base URL of the TerriaJS server proxy, to which requests will be appended. In most cases this is the server's
     * host + '/proxy'.
     * @type {String}
     */
    this.baseProxyUrl = undefined;
    /**
     *  Domains that should be proxied for, as set by config files. Stored as an array of hosts - if a TLD is specified,
     * subdomains will also be proxied.
     *  @type {String[]}
     */
    this.proxyDomains = undefined;
    /**
     * True if we expect that the proxy will proxy any URL - note that if the server isn't set up to do this, having
     * this set to true will just result in a lot of failed AJAX calls
     * @type {boolean}
     */
    this.isOpenProxy = false;
    /**
     * Domains that are known to support CORS, as set by config files.
     * @type {String[]}
     */
    this.corsDomains = [];
    /**
     * Whether the proxy should be used regardless of whether the domain supports CORS or not. This defaults to true
     * on IE<10.
     * @type {boolean}
     */
    this.alwaysUseProxy = FeatureDetection.isInternetExplorer() && FeatureDetection.internetExplorerVersion()[0] < 10; // IE versions prior to 10 don't support CORS, so always use the proxy.
    /**
     * Whether the page that Terria is running on is HTTPS. This is relevant because calling an HTTP domain from HTTPS
     * results in mixed content warnings and going through the proxy is required to get around this.
     * @type {boolean}
     */
    this.pageIsHttps = typeof window !== 'undefined' && defined(window.location) && defined(window.location.href) && new URI(window.location.href).protocol() === 'https';
}

/**
 * Initialises values with config previously loaded from server. This is the recommended way to use this object as it ensures
 * the options will be correct for the proxy server it's configured to call, but this can be skipped and the values it
 * initialises set manually if desired.
 *
 * @param {Object} serverConfig Configuration options retrieved from a ServerConfig object.
 * @param {String} baseProxyUrl The base URL to proxy with - this will default to 'proxy/'
 * @param {String[]} proxyDomains Initial value for proxyDomains to which proxyable domains from the server will be appended -
 *      defaults to an empty array.
 * @returns {Promise} A promise that resolves when initialisation is complete.
 */
CorsProxy.prototype.init = function(serverConfig, baseProxyUrl, proxyDomains) {
    this.baseProxyUrl = defaultValue(baseProxyUrl, DEFAULT_BASE_PROXY_PATH);
    this.proxyDomains = defaultValue(proxyDomains, []);
    if (serverConfig && typeof serverConfig === 'object') {
        this.isOpenProxy = !! serverConfig.proxyAllDomains;
        // ignore client list of allowed proxies in favour of definitive server list.
        if (Array.isArray(serverConfig.allowProxyFor)) {
            this.proxyDomains = serverConfig.allowProxyFor;
        }
    }
};

/**
 * Determines if the proxying service should be used to access the given URL, based on our list of
 * domains we're willing to proxy for and hosts that are known to support CORS.
 *
 * @param {String} url The url to examine.
 * @return {Boolean} true if the proxy should be used, false if not.
 */
CorsProxy.prototype.shouldUseProxy = function(url) {
    if (!defined(url)) {
        // eg. no url may be passed if all data is embedded
        return false;
    }

    var uri = new URI(url);
    var host = uri.host();

    if (host === '') {
        // do not proxy local files
        return false;
    }

    if (!this.isOpenProxy && !hostInDomains(host, this.proxyDomains)) {
        // we're not willing to proxy for this host
        return false;
    }
    if (this.alwaysUseProxy) {
        return true;
    }

    if (this.pageIsHttps && uri.protocol() === 'http') {
        // if we're accessing an http resource from an https page, always proxy in order to avoid a mixed content error.
        return true;
    }

    if (hostInDomains(host, this.corsDomains)) {
        // we don't need to proxy for this host, because it supports CORS
        return false;
    }

    // we are ok with proxying for this host and we need to
    return true;
};

/**
 * Proxies a URL by appending it to {@link CorsProxy#baseProxyUrl}. Optionally inserts a proxyFlag that will override
 * the cache headers of the response, allowing for caching to be added where it wouldn't otherwise.
 *
 * @param {String} resource the URL to potentially proxy
 * @param {String} proxyFlag the proxy flag to pass - generally this is the length of time that you want to override
 *       the cache headers with. E.g. '2d' for 2 days.
 * @returns {String} The proxied URL
 */
CorsProxy.prototype.getURL = function (resource, proxyFlag) {
    var flag = (proxyFlag === undefined) ? '' : '_' + proxyFlag + '/';
    return this.baseProxyUrl + flag + resource;
};

/**
 * Convenience method that combines {@link CorsProxy#shouldUseProxy} and {@link getURL} - if the URL passed needs to be
 * proxied according to the rules/config of the proxy, this will return a proxied URL, otherwise it will return the
 * original URL.
 *
 * {@see CorsProxy#shouldUseProxy}
 * {@see CorsProxy#getURL}
 *
 * @param {String} resource the URL to potentially proxy
 * @param {String} proxyFlag the proxy flag to pass - generally this is the length of time that you want to override
 *       the cache headers with. E.g. '2d' for 2 days.
 * @returns {String} Either the URL passed in or a proxied URL if it should be proxied.
 */
CorsProxy.prototype.getURLProxyIfNecessary = function (resource, proxyFlag) {
    if (this.shouldUseProxy(resource)) {
        return this.getURL(resource, proxyFlag);
    }

    return resource;
};

/**
 * Determines whether this host is, or is a subdomain of, an item in the provided array.
 *
 * @param {String} host The host to search for
 * @param {String[]} domains The array of domains to look in
 * @returns {boolean} The result.
 */
function hostInDomains(host, domains) {
    if (!defined(domains)) {
        return false;
    }

    host = host.toLowerCase();
    for (var i = 0; i < domains.length; i++) {
        if (host.match("(^|\\.)" + domains[i] + "$")) {
            return true;
        }
    }
    return false;
}

module.exports = CorsProxy;