From 8b2d33e6c84e5d865fd31695d7eebae9fb778eca Mon Sep 17 00:00:00 2001 From: RowHeat <40065760+rowheat02@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:41:05 +0545 Subject: [PATCH] Fix #10505 Allow to specify use of proxy or cors at layer level (#10526) * fix: remove ui element for force proxy and Allow not secure layers * fix: ajax logic changed, autoDetectCORS is set to true by default * new central CORS util file created and used in ajax * checking CORS before adding in common layer file * null check on getProxyUrl * updated individual layer considring to use proxy if needed * avoid proxy cache to update if response is not okey * enable user to add http url, show warning instead of error, warning text updated * test cases updated * fix: resolve conflicts with url check * fixed the failed test * review cesium layers * include add method in model layer * improve http check for openlayers wms layer * fix tests --------- Co-authored-by: allyoucanmap --- web/client/api/CORS.js | 67 ++++ .../TOC/fragments/settings/Display.jsx | 8 - .../settings/__tests__/Display-test.jsx | 84 +---- .../CommonAdvancedSettings.jsx | 9 +- .../RasterAdvancedSettings.js | 7 - .../__tests__/CommonAdvancedSettings-test.js | 28 +- .../__tests__/RasterAdvancedSettings-test.js | 30 +- .../components/catalog/editor/MainForm.jsx | 21 +- .../catalog/editor/MainFormUtils.js | 4 +- .../editor/__tests__/MainFormUtils-test.js | 26 -- web/client/components/map/cesium/Layer.jsx | 53 +++- .../map/cesium/__tests__/Layer-test.jsx | 290 ++++++++++++------ .../map/cesium/__tests__/Map-test.jsx | 74 +++-- .../map/cesium/plugins/ArcGISLayer.js | 17 +- .../map/cesium/plugins/ModelLayer.js | 29 +- .../map/cesium/plugins/TerrainLayer.js | 12 +- .../map/cesium/plugins/ThreeDTilesLayer.js | 124 ++++---- .../map/cesium/plugins/TileProviderLayer.js | 22 +- .../components/map/cesium/plugins/WFSLayer.js | 108 +++---- .../components/map/cesium/plugins/WMSLayer.js | 2 +- .../map/cesium/plugins/WMTSLayer.js | 7 +- web/client/libs/__tests__/ajax-test.js | 17 +- web/client/libs/ajax.js | 66 ++-- .../observables/wps/__tests__/execute-test.js | 3 +- web/client/translations/data.da-DK.json | 2 +- web/client/translations/data.de-DE.json | 2 +- web/client/translations/data.en-US.json | 2 +- web/client/translations/data.es-ES.json | 2 +- web/client/translations/data.fr-FR.json | 2 +- web/client/translations/data.is-IS.json | 2 +- web/client/translations/data.it-IT.json | 2 +- web/client/translations/data.nl-NL.json | 2 +- web/client/translations/data.sk-SK.json | 2 +- web/client/translations/data.sv-SE.json | 2 +- web/client/utils/ConfigUtils.js | 2 +- web/client/utils/cesium/WMSUtils.js | 4 +- .../utils/cesium/__tests__/WMSUtils-test.js | 2 +- .../utils/mapinfo/__tests__/arcgis-test.js | 6 +- web/client/utils/openlayers/WMSUtils.js | 18 +- 39 files changed, 644 insertions(+), 516 deletions(-) create mode 100644 web/client/api/CORS.js diff --git a/web/client/api/CORS.js b/web/client/api/CORS.js new file mode 100644 index 0000000000..c6b00adbff --- /dev/null +++ b/web/client/api/CORS.js @@ -0,0 +1,67 @@ +/* + * Copyright 2024, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import url from 'url'; +import { needProxy } from '../utils/ProxyUtils'; + +let proxyCache = {}; + + +const getBaseUrl = (uri) => { + const urlParts = url.parse(uri); + return urlParts.protocol + "//" + urlParts.host + urlParts.pathname; +}; +/** + * Set the proxy value for cached uri + * @param {string} uri - uri string to test + * @param {boolean} value - value to cache + * @returns the passed value + */ +export const setProxyCacheByUrl = (uri, value)=>{ + const baseUrl = getBaseUrl(uri); + proxyCache[baseUrl] = value; + return value; +}; +/** + * Get the proxy value for cached uri + * @param {string} uri - uri string to test + * @returns true, false or undefined, if undefined means the value has not been stored + */ +export const getProxyCacheByUrl = (uri)=>{ + const baseUrl = getBaseUrl(uri); + return proxyCache[baseUrl]; +}; +/** + * Perform a fetch request to test if a service support CORS + * @param {string} uri - uri string to test + * @returns true if the proxy is required + */ +export const testCors = (uri) => { + const proxy = getProxyCacheByUrl(uri); + if (needProxy(uri) === false) { + setProxyCacheByUrl(uri, false); + return Promise.resolve(false); + } + if (proxy !== undefined) { + return Promise.resolve(proxy); + } + return fetch(uri, { + method: 'GET', + mode: 'cors' + }) + .then((response) => { + if (!response.ok) { + return false; + } + return setProxyCacheByUrl(uri, false); + }) + .catch(() => { + // in server side error it goes to response(then) anyway, so we can assume that if we get here we have a cors error with no previewable response + return setProxyCacheByUrl(uri, true); + }); +}; diff --git a/web/client/components/TOC/fragments/settings/Display.jsx b/web/client/components/TOC/fragments/settings/Display.jsx index 2f3ea6957d..84321639a0 100644 --- a/web/client/components/TOC/fragments/settings/Display.jsx +++ b/web/client/components/TOC/fragments/settings/Display.jsx @@ -243,14 +243,6 @@ export default class extends React.Component { onChange={(e) => this.props.onChange("localizedLayerStyles", e.target.checked)}>  } /> ))} - {!this.props.isCesiumActive && ( this.props.onChange("forceProxy", e.target.checked)} - checked={this.props.element.forceProxy} > - - )} {(this.props.element?.serverType !== ServerTypes.NO_VENDOR && ( <>
diff --git a/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx b/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx index 631ae8713a..2ae82acbaf 100644 --- a/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx +++ b/web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx @@ -77,7 +77,7 @@ describe('test Layer Properties Display module component', () => { expect(comp).toBeTruthy(); const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" ); expect(inputs).toBeTruthy(); - expect(inputs.length).toBe(14); + expect(inputs.length).toBe(13); ReactTestUtils.Simulate.focus(inputs[2]); expect(inputs[2].value).toBe('70'); inputs[8].click(); @@ -105,7 +105,7 @@ describe('test Layer Properties Display module component', () => { expect(comp).toBeTruthy(); const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" ); expect(inputs).toBeTruthy(); - expect(inputs.length).toBe(13); + expect(inputs.length).toBe(12); ReactTestUtils.Simulate.focus(inputs[2]); expect(inputs[2].value).toBe('70'); inputs[8].click(); @@ -199,64 +199,6 @@ describe('test Layer Properties Display module component', () => { expect(isLocalizedLayerStylesOption).toBeTruthy(); }); - it('tests Display component for wms with force proxy option displayed', () => { - const l = { - name: 'layer00', - title: 'Layer', - visibility: true, - storeIndex: 9, - type: 'wms', - url: 'fakeurl', - forceProxy: true - }; - const settings = { - options: {opacity: 0.7} - }; - ReactDOM.render(, document.getElementById("container")); - const isForceProxyOption = document.querySelector('[data-qa="display-forceProxy-option"]'); - expect(isForceProxyOption).toBeTruthy(); - }); - it('tests Display component for wms with force proxy option in cesium map', () => { - const l = { - name: 'layer00', - title: 'Layer', - visibility: true, - storeIndex: 9, - type: 'wms', - url: 'fakeurl', - forceProxy: true - }; - const settings = { - options: {opacity: 0.7} - }; - ReactDOM.render(, document.getElementById("container")); - const isForceProxyOption = document.querySelector('[data-qa="display-forceProxy-option"]'); - expect(isForceProxyOption).toBeFalsy(); - }); - it('tests Display component for wms with force proxy option onChange', () => { - const handlers = { - onChange() {} - }; - const spyOn = expect.spyOn(handlers, 'onChange'); - const l = { - name: 'layer00', - title: 'Layer', - visibility: true, - storeIndex: 9, - type: 'wms', - url: 'fakeurl', - forceProxy: false - }; - const settings = { - options: {opacity: 0.7} - }; - ReactDOM.render(, document.getElementById("container")); - const isForceProxyOption = document.querySelector('[data-qa="display-forceProxy-option"]'); - expect(isForceProxyOption).toBeTruthy(); - ReactTestUtils.Simulate.change(isForceProxyOption, { "target": { "checked": true }}); - expect(spyOn).toHaveBeenCalled(); - expect(spyOn.calls[0].arguments).toEqual([ 'forceProxy', true ]); - }); it('tests Layer Properties Legend component for map viewer only', () => { const l = { @@ -277,8 +219,8 @@ describe('test Layer Properties Display module component', () => { expect(comp).toBeTruthy(); const labels = ReactTestUtils.scryRenderedDOMComponentsWithClass( comp, "control-label" ); const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" ); - const legendWidth = inputs[12]; - const legendHeight = inputs[13]; + const legendWidth = inputs[11]; + const legendHeight = inputs[12]; // Default legend values expect(legendWidth.value).toBe('12'); expect(legendHeight.value).toBe('12'); @@ -307,8 +249,8 @@ describe('test Layer Properties Display module component', () => { expect(comp).toBeTruthy(); const labels = ReactTestUtils.scryRenderedDOMComponentsWithClass( comp, "control-label" ); const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" ); - const legendWidth = inputs[11]; - const legendHeight = inputs[12]; + const legendWidth = inputs[10]; + const legendHeight = inputs[11]; // Default legend values expect(legendWidth.value).toBe('12'); expect(legendHeight.value).toBe('12'); @@ -347,10 +289,10 @@ describe('test Layer Properties Display module component', () => { const legendPreview = ReactTestUtils.scryRenderedDOMComponentsWithClass( comp, "legend-preview" ); expect(legendPreview).toBeTruthy(); expect(inputs).toBeTruthy(); - expect(inputs.length).toBe(14); - let interactiveLegendConfig = inputs[11]; - let legendWidth = inputs[12]; - let legendHeight = inputs[13]; + expect(inputs.length).toBe(13); + let interactiveLegendConfig = inputs[10]; + let legendWidth = inputs[11]; + let legendHeight = inputs[12]; const img = ReactTestUtils.scryRenderedDOMComponentsWithTag(comp, 'img'); // Check value in img src @@ -423,8 +365,8 @@ describe('test Layer Properties Display module component', () => { expect(comp).toBeTruthy(); const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" ); expect(inputs).toBeTruthy(); - expect(inputs.length).toBe(14); - expect(inputs[12].value).toBe("20"); - expect(inputs[13].value).toBe("40"); + expect(inputs.length).toBe(13); + expect(inputs[11].value).toBe("20"); + expect(inputs[12].value).toBe("40"); }); }); diff --git a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx index 5368fe49b9..147e4127eb 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx @@ -39,14 +39,7 @@ export default ({ - {!isNil(service.type) && service.type === "wfs" && - - onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} - checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> -  } /> - - } + {!isNil(service.type) && service.type === "cog" &&  } /> } - {!isNil(service.type) && service.type === "wms" && - onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} - checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> -  } /> - - } {(!isNil(service.type) ? (service.type === "csw" && !service.excludeShowTemplate) : false) && ( onToggleTemplate()} diff --git a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js index 8cacf029fe..04a3eb57a2 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js +++ b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js @@ -38,7 +38,7 @@ describe('Test common advanced settings', () => { const advancedSettingPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingPanel).toBeTruthy(); const fields = document.querySelectorAll(".form-group"); - expect(fields.length).toBe(3); + expect(fields.length).toBe(2); }); it('test wms advanced options onChangeServiceProperty autoreload', () => { const action = { @@ -52,7 +52,7 @@ describe('Test common advanced settings', () => { const advancedSettingPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingPanel).toBeTruthy(); const fields = document.querySelectorAll(".form-group"); - expect(fields.length).toBe(3); + expect(fields.length).toBe(2); const autoload = document.querySelectorAll('input[type="checkbox"]')[0]; const formGroup = document.querySelectorAll('.form-group')[0]; expect(formGroup.textContent.trim()).toBe('catalog.autoload'); @@ -61,30 +61,6 @@ describe('Test common advanced settings', () => { expect(spyOn).toHaveBeenCalled(); expect(spyOn.calls[0].arguments).toEqual([ 'autoload', true ]); }); - it('test component onChangeServiceProperty allowUnsecureLayers', () => { - const action = { - onChangeServiceProperty: () => {} - }; - const spyOn = expect.spyOn(action, 'onChangeServiceProperty'); - ReactDOM.render(, document.getElementById("container")); - const advancedSettingsPanel = document.getElementsByClassName("mapstore-switch-panel"); - expect(advancedSettingsPanel).toBeTruthy(); - const allowUnsecureLayers = document.querySelectorAll('input[type="checkbox"]')[1]; - const formGroup = document.querySelectorAll('.form-group')[2]; - expect(formGroup.textContent.trim()).toBe('catalog.allowUnsecureLayers.label'); - expect(allowUnsecureLayers).toExist(); - TestUtils.Simulate.change(allowUnsecureLayers, { "target": { "checked": true }}); - expect(spyOn).toHaveBeenCalled(); - expect(spyOn.calls[0].arguments).toEqual([ 'allowUnsecureLayers', true ]); - - // Unset allowUnsecureLayers - TestUtils.Simulate.change(allowUnsecureLayers, { "target": { "checked": false }}); - expect(spyOn).toHaveBeenCalled(); - expect(spyOn.calls[1].arguments).toEqual([ 'allowUnsecureLayers', false ]); - }); it('test component onChangeServiceProperty fetchMetadata', () => { const action = { onChangeServiceProperty: () => {} diff --git a/web/client/components/catalog/editor/AdvancedSettings/__tests__/RasterAdvancedSettings-test.js b/web/client/components/catalog/editor/AdvancedSettings/__tests__/RasterAdvancedSettings-test.js index 3f8b726472..b32859c2e2 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/__tests__/RasterAdvancedSettings-test.js +++ b/web/client/components/catalog/editor/AdvancedSettings/__tests__/RasterAdvancedSettings-test.js @@ -36,7 +36,7 @@ describe('Test Raster advanced settings', () => { const advancedSettingPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingPanel).toBeTruthy(); const fields = document.querySelectorAll(".form-group"); - expect(fields.length).toBe(15); + expect(fields.length).toBe(14); // check disabled refresh button }); @@ -45,7 +45,7 @@ describe('Test Raster advanced settings', () => { const advancedSettingPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingPanel).toBeTruthy(); const fields = document.querySelectorAll(".form-group"); - expect(fields.length).toBe(13); + expect(fields.length).toBe(12); const refreshButton = document.querySelectorAll('button')[0]; expect(refreshButton).toBeTruthy(); expect(refreshButton.disabled).toBe(false); @@ -214,30 +214,6 @@ describe('Test Raster advanced settings', () => { expect(spyOn).toHaveBeenCalled(); expect(spyOn.calls[0].arguments).toEqual([ 'layerOptions', { tileSize: 512 } ]); }); - it('test component onChangeServiceProperty allowUnsecureLayers', () => { - const action = { - onChangeServiceProperty: () => {} - }; - const spyOn = expect.spyOn(action, 'onChangeServiceProperty'); - ReactDOM.render(, document.getElementById("container")); - const advancedSettingsPanel = document.getElementsByClassName("mapstore-switch-panel"); - expect(advancedSettingsPanel).toBeTruthy(); - const allowUnsecureLayers = document.querySelectorAll('input[type="checkbox"]')[3]; - const formGroup = document.querySelectorAll('.form-group')[4]; - expect(formGroup.textContent.trim()).toBe('catalog.allowUnsecureLayers.label'); - expect(allowUnsecureLayers).toBeTruthy(); - TestUtils.Simulate.change(allowUnsecureLayers, { "target": { "checked": true }}); - expect(spyOn).toHaveBeenCalled(); - expect(spyOn.calls[0].arguments).toEqual([ 'allowUnsecureLayers', true ]); - - // Unset allowUnsecureLayers - TestUtils.Simulate.change(allowUnsecureLayers, { "target": { "checked": false }}); - expect(spyOn).toHaveBeenCalled(); - expect(spyOn.calls[1].arguments).toEqual([ 'allowUnsecureLayers', false ]); - }); it('test component onChangeServiceProperty useCacheOption for remote tile grids', () => { const action = { onChangeServiceProperty: () => {} @@ -249,7 +225,7 @@ describe('Test Raster advanced settings', () => { />, document.getElementById("container")); const advancedSettingsPanel = document.getElementsByClassName("mapstore-switch-panel"); expect(advancedSettingsPanel).toBeTruthy(); - const formGroup = document.querySelectorAll('.form-group')[7]; + const formGroup = document.querySelectorAll('.form-group')[6]; expect(formGroup.textContent.trim()).toBe('layerProperties.useCacheOptionInfo.label'); const useCacheOption = formGroup.querySelector('input[type="checkbox"]'); expect(useCacheOption).toBeTruthy(); diff --git a/web/client/components/catalog/editor/MainForm.jsx b/web/client/components/catalog/editor/MainForm.jsx index 3ec7ff0e83..42a22c31cd 100644 --- a/web/client/components/catalog/editor/MainForm.jsx +++ b/web/client/components/catalog/editor/MainForm.jsx @@ -5,8 +5,8 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -import React, { useState, useEffect } from 'react'; -import {get, find, isEmpty} from 'lodash'; +import React, { useState } from 'react'; +import {get, find} from 'lodash'; import Message from '../../I18N/Message'; import HTML from '../../I18N/HTML'; @@ -154,17 +154,21 @@ export default ({ setValid = () => {} }) => { const [error, setError] = useState(null); + const [warning, setWarning] = useState(null); function handleProtocolValidity(url) { onChangeUrl(url); if (url) { const {valid, errorMsgId} = checkUrl(url, null, service?.allowUnsecureLayers); - setError(valid ? null : errorMsgId); - setValid(valid); + if (errorMsgId === "catalog.invalidUrlHttpProtocol") { + setError(null); + setWarning(errorMsgId); + } else { + setWarning(null); + setError(valid ? null : errorMsgId); + setValid(valid); + } } } - useEffect(() => { - !isEmpty(service.url) && handleProtocolValidity(service.url); - }, [service?.allowUnsecureLayers]); const URLEditor = service.type === "tms" ? TmsURLEditor : service.type === "cog" ? COGEditor : DefaultURLEditor; return (
@@ -195,6 +199,9 @@ export default ({ {error ? : null} + {warning ? + + : null}
); }; diff --git a/web/client/components/catalog/editor/MainFormUtils.js b/web/client/components/catalog/editor/MainFormUtils.js index a2d16a13d5..bd297ef9c0 100644 --- a/web/client/components/catalog/editor/MainFormUtils.js +++ b/web/client/components/catalog/editor/MainFormUtils.js @@ -27,12 +27,12 @@ export const defaultPlaceholder = (service) => { * @param {boolean} allowUnsecureLayers flag to allow unsecure url * @returns {object} {valid: boolean, errorMsgId: string} */ -export const checkUrl = (catalogUrl = '', currentLocation, allowUnsecureLayers) => { +export const checkUrl = (catalogUrl = '', currentLocation) => { try { const { protocol: mapStoreProtocol } = url.parse(currentLocation ?? window.location.href); const { protocol: catalogProtocol } = url.parse(catalogUrl); if (mapStoreProtocol === 'https:' && !!catalogProtocol) { - const isProtocolValid = (mapStoreProtocol === catalogProtocol || allowUnsecureLayers); + const isProtocolValid = (mapStoreProtocol === catalogProtocol); return isProtocolValid ? {valid: true} : {valid: false, errorMsgId: "catalog.invalidUrlHttpProtocol"}; } return {valid: true}; diff --git a/web/client/components/catalog/editor/__tests__/MainFormUtils-test.js b/web/client/components/catalog/editor/__tests__/MainFormUtils-test.js index dd67581e79..1b2e8d4b54 100644 --- a/web/client/components/catalog/editor/__tests__/MainFormUtils-test.js +++ b/web/client/components/catalog/editor/__tests__/MainFormUtils-test.js @@ -29,30 +29,4 @@ describe('Catalog Main Form Editor Utils', () => { expect(messageId).toEqual(errorMsgId); }); }); - it('checkUrl with allowUnsecureLayers', () => { - const URLS = [ - // http - ['http://myDomain.com/geoserver/wms', 'https://myMapStore.com/geoserver/wms', true, true], - ['http://myDomain.com/geoserver/wms', 'http://myMapStore.com/geoserver/wms', true, false], - // https - ['https://myDomain.com/geoserver/wms', 'http://myMapStore.com/geoserver/wms', true, true], - ['https://myDomain.com/geoserver/wms', 'https://myMapStore.com/geoserver/wms', true, false], - // protocol relative URL - ['//myDomain.com/geoserver/wms', 'http://myMapStore.com/geoserver/wms', true, false], - ['//myDomain.com/geoserver/wms', 'https://myMapStore.com/geoserver/wms', true, false], - // absolute path - ['/geoserver/wms', 'http://myMapStore.com/geoserver/wms', true, false], - ['/geoserver/wms', 'https://myMapStore.com/geoserver/wms', true, false], - // relative path - ["geoserver/wms", "http://myMapStore.com/geoserver/wms", true, false], - ["geoserver/wms", "https://myMapStore.com/geoserver/wms", true, true], - [["geoserver/wms", "geoserver/wms"], "https://myMapStore.com/geoserver/wms", false, false, "catalog.invalidArrayUsageForUrl"] // array - ]; - URLS.forEach(([catalogURL, locationURL, valid, allowUnsecureLayers, messageId]) => { - const {valid: isValid, errorMsgId} = checkUrl(catalogURL, locationURL, allowUnsecureLayers); - expect(!!isValid).toEqual(!!valid, `${catalogURL} - added when location is ${locationURL} should be ${valid}, but it is ${isValid}`); - expect(messageId).toEqual(errorMsgId); - expect(!!isValid).toEqual(!!valid, `${catalogURL} - added when location is ${locationURL} should be ${valid}, but it is ${isValid}`); - }); - }); }); diff --git a/web/client/components/map/cesium/Layer.jsx b/web/client/components/map/cesium/Layer.jsx index bead9e5ee9..64332bf86f 100644 --- a/web/client/components/map/cesium/Layer.jsx +++ b/web/client/components/map/cesium/Layer.jsx @@ -10,8 +10,9 @@ import React from 'react'; import Layers from '../../../utils/cesium/Layers'; import assign from 'object-assign'; import PropTypes from 'prop-types'; -import { round, isNil } from 'lodash'; +import { round, isNil, castArray } from 'lodash'; import { getResolutions } from '../../../utils/MapUtils'; +import { testCors, getProxyCacheByUrl } from '../../../api/CORS'; class CesiumLayer extends React.Component { static propTypes = { @@ -146,6 +147,9 @@ class CesiumLayer extends React.Component { ...props.options, visibility }, props.position, props.map, props.securityToken); + if (this.layer.add) { + this.layer.add(); + } return; } // while hidden layers will be completely removed @@ -210,9 +214,18 @@ class CesiumLayer extends React.Component { createLayer = (type, options, position, map, securityToken) => { if (type) { - const opts = assign({}, options, position ? {zIndex: position} : null, {securityToken}); + const isProxy = options?.url ? getProxyCacheByUrl(castArray(options.url)[0]) : undefined; + if (isProxy !== undefined) { + this._isProxy = isProxy; + this._prevIsProxy = this._isProxy; + } + const opts = { + ...options, + ...(position ? { zIndex: position } : null), + securityToken, + ...(this._isProxy ? { forceProxy: this._isProxy } : null) + }; this.layer = Layers.createLayer(type, opts, map); - if (this.layer) { this.layer.layerName = options.name; this.layer.layerId = options.id; @@ -225,7 +238,20 @@ class CesiumLayer extends React.Component { }; updateLayer = (newProps, oldProps) => { - const newLayer = Layers.updateLayer(newProps.type, this.layer, {...newProps.options, securityToken: newProps.securityToken}, {...oldProps.options, securityToken: oldProps.securityToken}, this.props.map); + const newLayer = Layers.updateLayer( + newProps.type, + this.layer, + { + ...newProps.options, + securityToken: newProps.securityToken, + forceProxy: this._isProxy + }, + { + ...oldProps.options, + securityToken: oldProps.securityToken, + forceProxy: this._prevIsProxy + }, + this.props.map); if (newLayer) { this.removeLayer(); this.layer = newLayer; @@ -233,6 +259,7 @@ class CesiumLayer extends React.Component { this.addLayer(newProps); } } + this.updateZIndex(newProps.position); newProps.map.scene.requestRender(); }; @@ -249,7 +276,7 @@ class CesiumLayer extends React.Component { newProps.map.scene.requestRender(); }; - addLayer = (newProps) => { + _addLayer = (newProps) => { // detached layers are layers that do not work through a provider // for this reason they cannot be added or removed from the map imageryProviders if (this.layer && !this.layer.detached) { @@ -265,8 +292,24 @@ class CesiumLayer extends React.Component { }, this.props.options.refresh); } } + if (this.layer?.detached && this.layer?.add) { + this.layer.add(); + } }; + addLayer = (newProps) => { + if (this._isProxy === undefined && newProps?.options?.url) { + const urls = castArray(newProps.options.url); + return testCors(urls[0]) + .then((isProxy) => { + this._isProxy = isProxy; + this.updateLayer(newProps, this.props); + this._prevIsProxy = this._isProxy; + }); + } + return this._addLayer(newProps); + } + removeLayer = (provider) => { const toRemove = provider || this.provider; if (toRemove) { diff --git a/web/client/components/map/cesium/__tests__/Layer-test.jsx b/web/client/components/map/cesium/__tests__/Layer-test.jsx index 573d8fd830..a8fbac721d 100644 --- a/web/client/components/map/cesium/__tests__/Layer-test.jsx +++ b/web/client/components/map/cesium/__tests__/Layer-test.jsx @@ -148,14 +148,14 @@ describe('Cesium layer', () => { expect(layer).toExist(); }); - it('creates a wms layer for Cesium map', () => { + it('creates a wms layer for Cesium map', (done) => { var options = { "type": "wms", "visibility": true, "name": "nurc:Arc_Sample", "group": "Meteo", "format": "image/png", - "url": "http://demo.geo-solutions.it/geoserver/wms" + "url": "/geoserver/wms" }; // create layers var layer = ReactDOM.render( @@ -163,10 +163,16 @@ describe('Cesium layer', () => { options={options} map={map}/>, document.getElementById("container")); expect(layer).toExist(); - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('{s}'); - expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._subdomains.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toExist(); + + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('{s}'); + expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._subdomains.length).toBe(1); + expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toBeFalsy(); + done(); + }).catch(done); + }); it('test wms vector formats must change to default image format (image/png)', () => { @@ -224,14 +230,14 @@ describe('Cesium layer', () => { expect(layer.layer._tileProvider._resource._queryParameters.format).toBe('image/jpeg'); }); - it('wms layer with credits', () => { + it('wms layer with credits', (done) => { var options = { "type": "wms", "visibility": true, "name": "nurc:Arc_Sample", "group": "Meteo", "format": "image/png", - "url": "http://demo.geo-solutions.it/geoserver/wms", + "url": "/geoserver/wms", credits: { imageUrl: "test.png", title: "test" @@ -243,10 +249,15 @@ describe('Cesium layer', () => { options={options} map={map}/>, document.getElementById("container")); expect(layer).toExist(); - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0].imageryProvider.credit).toExist(); + + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0].imageryProvider.credit).toExist(); + done(); + }).catch(done); }); - it('creates a wms layer with caching for Cesium map', () => { + it('creates a wms layer with caching for Cesium map', (done) => { var options = { "type": "wms", "visibility": true, @@ -254,7 +265,7 @@ describe('Cesium layer', () => { "group": "Meteo", "format": "image/png", "tiled": true, - "url": "http://demo.geo-solutions.it/geoserver/wms" + "url": "/geoserver/wms" }; // create layers var layer = ReactDOM.render( @@ -262,13 +273,19 @@ describe('Cesium layer', () => { options={options} map={map}/>, document.getElementById("container")); expect(layer).toExist(); - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('{s}'); - expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._subdomains.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toExist(); - expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._resource._queryParameters.tiled).toBe(true); + + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('{s}'); + expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._subdomains.length).toBe(1); + expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toBeFalsy(); + expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._resource._queryParameters.tiled).toBe(true); + done(); + }).catch(done); + }); - it('check wms layer proxy skip for relative urls', () => { + it('check wms layer proxy skip for relative urls', (done) => { var options = { "type": "wms", "visibility": true, @@ -283,13 +300,18 @@ describe('Cesium layer', () => { options={options} map={map}/>, document.getElementById("container")); expect(layer).toExist(); - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('{s}'); - expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._subdomains.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toNotExist(); + + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('{s}'); + expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._subdomains.length).toBe(1); + expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toNotExist(); + done(); + }).catch(done); }); - it('creates a wmts layer for Cesium map', () => { + it('creates a wmts layer for Cesium map', (done) => { var options = { "type": "wmts", "visibility": true, @@ -305,21 +327,24 @@ describe('Cesium layer', () => { } }] }, - "url": "http://sample.server/geoserver/gwc/service/wmts" + "url": "/geoserver/gwc/service/wmts" }; // create layers var layer = ReactDOM.render( , document.getElementById("container")); - expect(layer).toExist(); - // count layers - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toExist(); - expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toExist(); + + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toExist(); + expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toBeFalsy(); + done(); + }).catch(done); }); - it('custom name tile set', () => { + it('custom name tile set', (done) => { var options = { "type": "wmts", "visibility": true, @@ -346,11 +371,15 @@ describe('Cesium layer', () => { expect(layer).toExist(); // count layers - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider._tileMatrixLabels).toExist(); - expect(map.imageryLayers._layers[0]._imageryProvider._tileMatrixLabels[0]).toBe("0"); + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0]._imageryProvider._tileMatrixLabels).toExist(); + expect(map.imageryLayers._layers[0]._imageryProvider._tileMatrixLabels[0]).toBe("0"); + done(); + }).catch(done); }); - it('check a wmts layer skips proxy config', () => { + it('check a wmts layer skips proxy config', (done) => { var options = { "type": "wmts", "visibility": true, @@ -373,13 +402,17 @@ describe('Cesium layer', () => { , document.getElementById("container")); expect(layer).toExist(); - // count layers - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toExist(); - expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toNotExist(); + + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toExist(); + expect(map.imageryLayers._layers[0]._imageryProvider.proxy.proxy).toBeFalsy(); + done(); + }).catch(done); }); - it('creates a wmts layer with custom credits for Cesium map', () => { + it('creates a wmts layer with custom credits for Cesium map', (done) => { var options = { "type": "wmts", "visibility": true, @@ -408,12 +441,15 @@ describe('Cesium layer', () => { expect(layer).toExist(); // count layers - expect(layer).toExist(); - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0].imageryProvider.credit).toExist(); + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0].imageryProvider.credit).toExist(); + done(); + }).catch(done); }); - it('creates a wms layer with single tile for CesiumLayer map', () => { + it('creates a wms layer with single tile for CesiumLayer map', (done) => { var options = { "type": "wms", "visibility": true, @@ -429,12 +465,17 @@ describe('Cesium layer', () => { options={options} map={map}/>, document.getElementById("container")); expect(layer).toExist(); - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe("http://demo.geo-solutions.it/geoserver/wms"); - expect(map.imageryLayers._layers[0]._imageryProvider._resource._queryParameters.service).toBe("WMS"); + + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe("http://demo.geo-solutions.it/geoserver/wms"); + expect(map.imageryLayers._layers[0]._imageryProvider._resource._queryParameters.service).toBe("WMS"); + done(); + }).catch(done); }); - it('creates a wms layer with multiple urls for CesiumLayer map', () => { + it('creates a wms layer with multiple urls for CesiumLayer map', (done) => { var options = { "type": "wms", "visibility": true, @@ -449,9 +490,13 @@ describe('Cesium layer', () => { options={options} map={map}/>, document.getElementById("container")); expect(layer).toExist(); - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('{s}'); - expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._subdomains.length).toBe(2); + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('{s}'); + expect(map.imageryLayers._layers[0]._imageryProvider._tileProvider._subdomains.length).toBe(2); + done(); + }).catch(done); }); it('creates a bing layer for cesium map', () => { @@ -490,7 +535,7 @@ describe('Cesium layer', () => { expect(map.imageryLayers.length).toBe(1); }); - it('changes wms layer opacity', () => { + it('changes wms layer opacity', (done) => { var options = { "type": "wms", "visibility": true, @@ -506,16 +551,21 @@ describe('Cesium layer', () => { options={options} position={0} map={map}/>, document.getElementById("container")); expect(layer).toExist(); - expect(map.imageryLayers.length).toBe(1); - expect(layer.provider.alpha).toBe(1.0); - layer = ReactDOM.render( - , document.getElementById("container")); - expect(layer.provider.alpha).toBe(0.5); + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + expect(layer.provider.alpha).toBe(1.0); + layer = ReactDOM.render( + , document.getElementById("container")); + expect(layer.provider.alpha).toBe(0.5); + done(); + }).catch(done); + }); - it('respects layer ordering 1', () => { + it('respects layer ordering 1', (done) => { var options1 = { "type": "wms", "visibility": true, @@ -532,7 +582,7 @@ describe('Cesium layer', () => { "group": "Meteo", "format": "image/png", "opacity": 1.0, - "url": "http://demo.geo-solutions.it/geoserver/wms" + "url": "/geoserver/wms" }; // create layers let layer1 = ReactDOM.render( @@ -541,7 +591,7 @@ describe('Cesium layer', () => { , document.getElementById("container")); expect(layer1).toExist(); - expect(map.imageryLayers.length).toBe(1); + // expect(map.imageryLayers.length).toBe(1); let layer2 = ReactDOM.render( { , document.getElementById("container2")); expect(layer2).toExist(); - expect(map.imageryLayers.length).toBe(2); layer1 = ReactDOM.render( { options={options2} map={map} position={1}/> , document.getElementById("container2")); - expect(map.imageryLayers.get(0)).toBe(layer2.provider); - expect(map.imageryLayers.get(1)).toBe(layer1.provider); + waitFor(() => { + return expect(map.imageryLayers.length).toBe(2); + }).then(() => { + expect(map.imageryLayers.get(0)).toBe(layer2.provider); + expect(map.imageryLayers.get(1)).toBe(layer1.provider); + done(); + }).catch(done); }); it('creates a graticule layer for cesium map', () => { @@ -675,7 +729,7 @@ describe('Cesium layer', () => { expect(map.entities._entities.length).toBe(1); }); - it('respects layer ordering 2', () => { + it('respects layer ordering 2', (done) => { var options = { "type": "wms", "visibility": true, @@ -683,7 +737,7 @@ describe('Cesium layer', () => { "group": "Meteo", "format": "image/png", "opacity": 1.0, - "url": "http://demo.geo-solutions.it/geoserver/wms" + "url": "/geoserver/wms" }; // create layers var layer = ReactDOM.render( @@ -692,8 +746,13 @@ describe('Cesium layer', () => { expect(layer).toExist(); - const position = map.imageryLayers.get(0)._position; - expect(position).toBe(10); + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + const position = map.imageryLayers.get(0)._position; + expect(position).toBe(10); + done(); + }).catch(done); }); it("test wms security token as bearer header", () => { const options = { @@ -1193,7 +1252,8 @@ describe('Cesium layer', () => { expect(map.imageryLayers.length).toBe(1); }); - it('Create a 3d tiles layer', () => { + + it('Create a 3d tiles layer', (done) => { const options = { type: '3dtiles', url: '/tileset.json', @@ -1217,10 +1277,15 @@ describe('Cesium layer', () => { map={map} />, document.getElementById('container')); expect(cmp).toBeTruthy(); - expect(cmp.layer.resource).toBeTruthy(); - expect(cmp.layer.resource.request.url).toBe('/tileset.json'); + waitFor(()=>{ + return expect(cmp.layer.getResource()).toBeTruthy(); + }).then(()=>{ + expect(cmp.layer.getResource().request.url).toBe('/tileset.json'); + done(); + }).catch(done); }); - it('Use proxy when needed', () => { + + it('Use proxy when needed', (done) => { const options = { type: '3dtiles', url: 'http://service.org/tileset.json', @@ -1234,7 +1299,8 @@ describe('Cesium layer', () => { maxx: 180, maxy: 90 } - } + }, + forceProxy: true }; // create layers const cmp = ReactDOM.render( @@ -1244,9 +1310,14 @@ describe('Cesium layer', () => { map={map} />, document.getElementById('container')); expect(cmp).toBeTruthy(); - expect(cmp.layer.resource).toBeTruthy(); - expect(cmp.layer.resource.request.url).toBe('/mapstore/proxy/?url=http%3A%2F%2Fservice.org%2Ftileset.json'); + waitFor(()=>{ + return expect(cmp.layer.getResource()).toBeTruthy(); + }).then(()=>{ + expect(cmp.layer.getResource().request.url).toBe('/mapstore/proxy/?url=http%3A%2F%2Fservice.org%2Ftileset.json'); + done(); + }).catch(done); }); + it('should create a 3d tiles layer with visibility set to false', () => { const options = { type: '3dtiles', @@ -1482,7 +1553,7 @@ describe('Cesium layer', () => { const options = { type: "wms", useForElevation: true, - url: "https://host-sample/geoserver/wms", + url: "/geoserver/wms", name: "workspace:layername", littleendian: false, visibility: true, @@ -1496,11 +1567,15 @@ describe('Cesium layer', () => { map={map} />, document.getElementById('container')); expect(cmp).toBeTruthy(); - expect(cmp.layer).toBeTruthy(); - cmp.layer.readyPromise.then(() => { - expect(cmp.layer._options.url).toEqual('https://host-sample/geoserver/wms'); - expect(cmp.layer._options.proxy.proxy).toBeTruthy(); - done(); + + waitFor(() => { + return expect(cmp.layer).toBeTruthy(); + }).then(() => { + cmp.layer.readyPromise.then(() => { + expect(cmp.layer._options.url).toEqual('/geoserver/wms'); + expect(cmp.layer._options.proxy.proxy).toBeFalsy(); + done(); + }).catch(done); }); }); @@ -1531,30 +1606,50 @@ describe('Cesium layer', () => { }); it('should create a bil terrain provider with wms config', (done) => { + const options = { type: "terrain", provider: "wms", - url: "https://host-sample/geoserver/wms", + url: "/geoserver/wms", name: "workspace:layername", littleendian: false, visibility: true, crs: 'CRS:84' }; - // create layers + + // Create layers const cmp = ReactDOM.render( , document.getElementById('container')); + + // Assert that component is rendered expect(cmp).toBeTruthy(); - expect(cmp.layer).toBeTruthy(); expect(cmp.layer.layerName).toBe(options.name); - cmp.layer.terrainProvider.readyPromise.then(() => { - expect(cmp.layer.terrainProvider._options.url).toEqual('https://host-sample/geoserver/wms'); - expect(cmp.layer.terrainProvider._options.proxy.proxy).toBeTruthy(); - done(); - }); + + + // Wait for the component's layer to be ready + waitFor(() => { + return expect(cmp.layer).toBeTruthy(); + }) + .then(() => { + + // Wait for the terrainProvider's readyPromise + cmp.layer.terrainProvider.readyPromise.then(() => { + expect(cmp.layer.terrainProvider._options.url).toEqual('/geoserver/wms'); + const proxy = cmp.layer.terrainProvider._options.proxy; + expect(proxy).toBeTruthy(); // Ensure proxy is defined + expect(proxy.proxy).toBeFalsy(); + done(); // Complete the test + }).catch(err => { + done(err); // In case of any errors + }); + }) + .catch(err => { + done(err); // Handle errors for waitFor + }); }); it('should create a bil terrain provider with wms config (no proxy url)', (done) => { @@ -1623,18 +1718,27 @@ describe('Cesium layer', () => { expect(cmp.layer).toBeTruthy(); expect(cmp.layer.getElevation).toBeTruthy(); }); - it('creates a arcgis layer', () => { + it('creates a arcgis layer', (done) => { const options = { type: 'arcgis', - url: 'http://arcgis/MapServer/', + url: '/arcgis/MapServer/', name: '1', visibility: true }; ReactDOM.render( , document.getElementById("container")); - expect(map.imageryLayers.length).toBe(1); - expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('http://arcgis/MapServer/'); - expect(map.imageryLayers._layers[0]._imageryProvider.layerName).toBe('1'); + + waitFor(() => { + return expect(map.imageryLayers.length).toBe(1); + }).then(() => { + try { + expect(map.imageryLayers._layers[0]._imageryProvider._resource._url).toBe('/arcgis/MapServer/'); + expect(map.imageryLayers._layers[0]._imageryProvider.layers).toBe('1'); + } catch (e) { + done(e); + } + done(); + }).catch(done); }); }); diff --git a/web/client/components/map/cesium/__tests__/Map-test.jsx b/web/client/components/map/cesium/__tests__/Map-test.jsx index 036c937623..77f311617e 100644 --- a/web/client/components/map/cesium/__tests__/Map-test.jsx +++ b/web/client/components/map/cesium/__tests__/Map-test.jsx @@ -20,6 +20,7 @@ import '../plugins/OSMLayer'; import '../plugins/WMSLayer'; import '../plugins/VectorLayer'; import '../plugins/ElevationLayer'; +import GeoServerBILTerrainProvider from '../../../../utils/cesium/GeoServerBILTerrainProvider'; import '../../../../utils/cesium/Layers'; import { @@ -106,9 +107,9 @@ describe('CesiumMap', () => { expect(ref.map.imageryLayers.length).toBe(1); }); - it('check layers for elevation (deprecated)', () => { + it('check layers for elevation (deprecated)', (done) => { const options = { - "url": "http://fake", + "url": "/endpoint", "name": "mylayer", "visibility": true, "useForElevation": true @@ -120,8 +121,14 @@ describe('CesiumMap', () => { , document.getElementById("container")); }); expect(ref).toBeTruthy(); - expect(ref.map.terrainProvider).toBeTruthy(); - expect(ref.map.terrainProvider.layerName).toBe('mylayer'); + waitFor(() => expect(ref.map.terrainProvider).toBeTruthy()).then(() => { + try { + expect(ref.map.terrainProvider instanceof GeoServerBILTerrainProvider).toBe(true); + } catch (e) { + done(e); + } + done(); + }).catch(done); }); it('check layers for elevation', () => { const options = { @@ -534,7 +541,7 @@ describe('CesiumMap', () => { // unregister hook registerHook(ZOOM_TO_EXTENT_HOOK); }); - it('should reorder the layer correctly even if the position property of layer exceed the imageryLayers length', () => { + it('should reorder the layer correctly even if the position property of layer exceed the imageryLayers length', (done) => { let ref; act(() => { @@ -549,34 +556,35 @@ describe('CesiumMap', () => { }); expect(ref).toBeTruthy(); - expect(ref.map.imageryLayers._layers.map(({ _position }) => _position)).toEqual([1, 3, 6]); - expect(ref.map.imageryLayers._layers.map(({ imageryProvider }) => imageryProvider.layers)).toEqual([ 'layer01', 'layer02', 'layer03' ]); - - act(() => { - ReactDOM.render( - { ref = value; } } id="mymap" center={{ y: 43.9, x: 10.3 }} zoom={11}> - - - - , - document.getElementById('container') - ); - }); - expect(ref.map.imageryLayers._layers.map(({ _position }) => _position)).toEqual([1, 3, 4]); - expect(ref.map.imageryLayers._layers.map(({ imageryProvider }) => imageryProvider.layers)).toEqual([ 'layer01', 'layer02', 'layer03' ]); - - act(() => { - ReactDOM.render( - { ref = value; } } id="mymap" center={{ y: 43.9, x: 10.3 }} zoom={11}> - - - - , - document.getElementById('container') - ); - }); - expect(ref.map.imageryLayers._layers.map(({ _position }) => _position)).toEqual([1, 2, 3]); - expect(ref.map.imageryLayers._layers.map(({ imageryProvider }) => imageryProvider.layers)).toEqual([ 'layer01', 'layer03', 'layer02' ]); + waitFor(() => expect(ref.map.imageryLayers._layers.length).toBe(3)).then(() => { + expect(ref.map.imageryLayers._layers.map(({ _position }) => _position)).toEqual([1, 3, 6]); + expect(ref.map.imageryLayers._layers.map(({ imageryProvider }) => imageryProvider.layers)).toEqual([ 'layer01', 'layer02', 'layer03' ]); + act(() => { + ReactDOM.render( + { ref = value; } } id="mymap" center={{ y: 43.9, x: 10.3 }} zoom={11}> + + + + , + document.getElementById('container') + ); + }); + expect(ref.map.imageryLayers._layers.map(({ _position }) => _position)).toEqual([1, 3, 4]); + expect(ref.map.imageryLayers._layers.map(({ imageryProvider }) => imageryProvider.layers)).toEqual([ 'layer01', 'layer02', 'layer03' ]); + act(() => { + ReactDOM.render( + { ref = value; } } id="mymap" center={{ y: 43.9, x: 10.3 }} zoom={11}> + + + + , + document.getElementById('container') + ); + }); + expect(ref.map.imageryLayers._layers.map(({ _position }) => _position)).toEqual([1, 2, 3]); + expect(ref.map.imageryLayers._layers.map(({ imageryProvider }) => imageryProvider.layers)).toEqual([ 'layer01', 'layer03', 'layer02' ]); + done(); + }).catch(done); }); it('should add navigation tools to the map', () => { let ref; diff --git a/web/client/components/map/cesium/plugins/ArcGISLayer.js b/web/client/components/map/cesium/plugins/ArcGISLayer.js index 1b78a21a07..293c9fccbf 100644 --- a/web/client/components/map/cesium/plugins/ArcGISLayer.js +++ b/web/client/components/map/cesium/plugins/ArcGISLayer.js @@ -9,6 +9,7 @@ import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; import { isImageServerUrl } from '../../../../utils/ArcGISUtils'; +import { getProxiedUrl } from '../../../../utils/ConfigUtils'; // this override is needed to apply the selected format // and to detect an ImageServer and to apply the correct exportImage path @@ -66,9 +67,9 @@ class ArcGisMapAndImageServerImageryProvider extends Cesium.ArcGisMapServerImage } } -Layers.registerType('arcgis', (options) => { +const create = (options) => { return new ArcGisMapAndImageServerImageryProvider({ - url: options.url, + url: options?.forceProxy ? getProxiedUrl() + encodeURIComponent(options.url) : options.url, ...(options.name !== undefined && { layers: `${options.name}` }), format: options.format, // we need to disable this when using layers ids @@ -76,4 +77,16 @@ Layers.registerType('arcgis', (options) => { // and render the map tiles representing all the layers available in the MapServer usePreCachedTilesIfAvailable: false }); +}; + +const update = (layer, newOptions, oldOptions) => { + if (newOptions.forceProxy !== oldOptions.forceProxy) { + return create(newOptions); + } + return null; +}; + +Layers.registerType('arcgis', { + create, + update: update }); diff --git a/web/client/components/map/cesium/plugins/ModelLayer.js b/web/client/components/map/cesium/plugins/ModelLayer.js index 106bb58f48..71f8525416 100644 --- a/web/client/components/map/cesium/plugins/ModelLayer.js +++ b/web/client/components/map/cesium/plugins/ModelLayer.js @@ -150,24 +150,27 @@ const createLayer = (options, map) => { return { detached: true, primitives: () => undefined, - remove: () => {} + remove: () => {}, + add: () => {} }; } - let primitives = new Cesium.PrimitiveCollection({ destroyPrimitives: true }); - getIFCModel(options.url) - .then(({ifcModule, data}) => { - const { meshes } = ifcDataToJSON({ ifcModule, data }); - const translucentPrimitive = createPrimitiveFromMeshes(meshes, options, 'translucentPrimitive'); - const opaquePrimitive = createPrimitiveFromMeshes(meshes, options, 'opaquePrimitive'); - primitives.add(translucentPrimitive); - primitives.add(opaquePrimitive); - updatePrimitivesMatrix(primitives, options?.features?.[0]); - - }); - map.scene.primitives.add(primitives); + let primitives; return { detached: true, primitives, + add: () => { + primitives = new Cesium.PrimitiveCollection({ destroyPrimitives: true }); + getIFCModel(options.url) + .then(({ifcModule, data}) => { + const { meshes } = ifcDataToJSON({ ifcModule, data }); + const translucentPrimitive = createPrimitiveFromMeshes(meshes, options, 'translucentPrimitive'); + const opaquePrimitive = createPrimitiveFromMeshes(meshes, options, 'opaquePrimitive'); + primitives.add(translucentPrimitive); + primitives.add(opaquePrimitive); + updatePrimitivesMatrix(primitives, options?.features?.[0]); + }); + map.scene.primitives.add(primitives); + }, remove: () => { if (primitives && map) { map.scene.primitives.remove(primitives); diff --git a/web/client/components/map/cesium/plugins/TerrainLayer.js b/web/client/components/map/cesium/plugins/TerrainLayer.js index 0351362f4b..0a14e0e737 100644 --- a/web/client/components/map/cesium/plugins/TerrainLayer.js +++ b/web/client/components/map/cesium/plugins/TerrainLayer.js @@ -10,10 +10,14 @@ import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; import GeoServerBILTerrainProvider from '../../../../utils/cesium/GeoServerBILTerrainProvider'; import WMSUtils from '../../../../utils/cesium/WMSUtils'; +import { getProxyUrl } from "../../../../utils/ProxyUtils"; function cesiumOptionsMapping(config) { return { - url: config.url, + url: new Cesium.Resource({ + url: config.url, + proxy: config.forceProxy ? new Cesium.DefaultProxy(getProxyUrl()) : undefined + }), credit: config?.options?.credit, ellipsoid: config?.options?.ellipsoid, requestMetadata: config?.options?.requestMetadata, @@ -42,10 +46,12 @@ const createLayer = (config, map) => { terrainProvider = new Cesium.EllipsoidTerrainProvider(); break; } - map.terrainProvider = terrainProvider; return { detached: true, terrainProvider, + add: () => { + map.terrainProvider = terrainProvider; + }, remove: () => { map.terrainProvider = new Cesium.EllipsoidTerrainProvider(); } @@ -55,7 +61,7 @@ const createLayer = (config, map) => { const updateLayer = (layer, newOptions, oldOptions, map) => { if (newOptions.securityToken !== oldOptions.securityToken || oldOptions.credits !== newOptions.credits - || oldOptions.provider !== newOptions.provider) { + || oldOptions.provider !== newOptions.provider || oldOptions.forceProxy !== newOptions.forceProxy) { return createLayer(newOptions, map); } return null; diff --git a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js index 6593b95b55..6e2e06c884 100644 --- a/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js +++ b/web/client/components/map/cesium/plugins/ThreeDTilesLayer.js @@ -11,7 +11,7 @@ import * as Cesium from 'cesium'; import isEqual from 'lodash/isEqual'; import isNumber from 'lodash/isNumber'; import isNaN from 'lodash/isNaN'; -import { getProxyUrl, needProxy } from "../../../../utils/ProxyUtils"; +import { getProxyUrl } from "../../../../utils/ProxyUtils"; import { getStyleParser } from '../../../../utils/VectorStyleUtils'; import { polygonToClippingPlanes } from '../../../../utils/cesium/PrimitivesUtils'; import tinycolor from 'tinycolor2'; @@ -134,69 +134,81 @@ function updateShading(tileSet, options, map) { setTimeout(() => map.scene.requestRender()); } -Layers.registerType('3dtiles', { - create: (options, map) => { - if (!options.visibility) { - return { - detached: true, - getTileSet: () => undefined, - remove: () => {} - }; - } - let tileSet; - const resource = new Cesium.Resource({ - url: options.url, - proxy: needProxy(options.url) ? new Cesium.DefaultProxy(getProxyUrl()) : undefined - // TODO: axios supports also adding access tokens or credentials (e.g. authkey, Authentication header ...). - // if we want to use internal cesium functionality to retrieve data - // we need to create a utility to set a CesiumResource that applies also this part. - // in addition to this proxy. - }); - let promise = Cesium.Cesium3DTileset.fromUrl(resource, - { - showCreditsOnScreen: true - } - ).then((_tileSet) => { - tileSet = _tileSet; - updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet); - map.scene.primitives.add(tileSet); - // assign the original mapstore id of the layer - tileSet.msId = options.id; - - ensureReady(tileSet, () => { - updateModelMatrix(tileSet, options); - clip3DTiles(tileSet, options, map); - updateShading(tileSet, options, map); - getStyle(options) - .then((style) => { - if (style) { - tileSet.style = new Cesium.Cesium3DTileStyle(style); - } - }); - }); - }); - const removeTileset = () => { - updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet); - map.scene.primitives.remove(tileSet); - tileSet = undefined; - }; +const createLayer = (options, map) => { + if (!options.visibility) { return { detached: true, - getTileSet: () => tileSet, - resource, - remove: () => { - if (tileSet) { - removeTileset(); - return; + getResource: () => undefined, + getTileSet: () => undefined, + add: () => {}, + remove: () => {} + }; + } + let tileSet; + let resource; + let promise; + const removeTileset = () => { + updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet); + map.scene.primitives.remove(tileSet); + tileSet = undefined; + }; + return { + detached: true, + getTileSet: () => tileSet, + getResource: () => resource, + add: () => { + resource = new Cesium.Resource({ + url: options.url, + proxy: options.forceProxy ? new Cesium.DefaultProxy(getProxyUrl()) : undefined + // TODO: axios supports also adding access tokens or credentials (e.g. authkey, Authentication header ...). + // if we want to use internal cesium functionality to retrieve data + // we need to create a utility to set a CesiumResource that applies also this part. + // in addition to this proxy. + }); + promise = Cesium.Cesium3DTileset.fromUrl(resource, + { + showCreditsOnScreen: true } + ).then((_tileSet) => { + tileSet = _tileSet; + updateGooglePhotorealistic3DTilesBrandLogo(map, options, tileSet); + map.scene.primitives.add(tileSet); + // assign the original mapstore id of the layer + tileSet.msId = options.id; + ensureReady(tileSet, () => { + updateModelMatrix(tileSet, options); + clip3DTiles(tileSet, options, map); + updateShading(tileSet, options, map); + getStyle(options) + .then((style) => { + if (style) { + tileSet.style = new Cesium.Cesium3DTileStyle(style); + } + }); + }); + }); + }, + remove: () => { + if (tileSet) { + removeTileset(); + return; + } + if (promise) { promise.then(() => { removeTileset(); }); - return; } - }; - }, + return; + } + }; +}; + +Layers.registerType('3dtiles', { + create: createLayer, update: function(layer, newOptions, oldOptions, map) { + if (newOptions.forceProxy !== oldOptions.forceProxy) { + return createLayer(newOptions, map); + } const tileSet = layer?.getTileSet(); if ( (!isEqual(newOptions.clippingPolygon, oldOptions.clippingPolygon) diff --git a/web/client/components/map/cesium/plugins/TileProviderLayer.js b/web/client/components/map/cesium/plugins/TileProviderLayer.js index cdd0ddb7e0..22fb0a77ce 100644 --- a/web/client/components/map/cesium/plugins/TileProviderLayer.js +++ b/web/client/components/map/cesium/plugins/TileProviderLayer.js @@ -11,7 +11,7 @@ import * as Cesium from 'cesium'; import TileProvider from '../../../../utils/TileConfigProvider'; import ConfigUtils from '../../../../utils/ConfigUtils'; import {creditsToAttribution} from '../../../../utils/LayersUtils'; -import {getProxyUrl, needProxy} from '../../../../utils/ProxyUtils'; +import {getProxyUrl} from '../../../../utils/ProxyUtils'; function splitUrl(originalUrl) { let url = originalUrl; @@ -74,13 +74,9 @@ export function template(str, data) { }); } -Layers.registerType('tileprovider', (options) => { +const create = (options) => { let [url, opt] = TileProvider.getLayerConfig(options.provider, options); let proxyUrl = ConfigUtils.getProxyUrl({}); - let proxy; - if (proxyUrl) { - proxy = opt.noCors || needProxy(url); - } const cr = opt.credits; const credit = cr ? new Cesium.Credit(creditsToAttribution(cr)) : opt.attribution; @@ -91,6 +87,18 @@ Layers.registerType('tileprovider', (options) => { maximumLevel: opt.maxZoom, minimumLevel: opt.minZoom, credit, - proxy: proxy ? new TileProviderProxy(proxyUrl) : new NoProxy() + proxy: options?.forceProxy ? new TileProviderProxy(proxyUrl) : new NoProxy() }); +}; + +const update = (layer, newOptions, oldOptions) => { + if (newOptions.forceProxy !== oldOptions.forceProxy) { + return create(newOptions); + } + return null; +}; + +Layers.registerType('tileprovider', { + create, + update: update }); diff --git a/web/client/components/map/cesium/plugins/WFSLayer.js b/web/client/components/map/cesium/plugins/WFSLayer.js index 516d3dec6d..39054e93e7 100644 --- a/web/client/components/map/cesium/plugins/WFSLayer.js +++ b/web/client/components/map/cesium/plugins/WFSLayer.js @@ -70,64 +70,68 @@ const createLayer = (options, map) => { opacity: options.opacity, queryable: options.queryable === undefined || options.queryable }); - - const loader = createLoader(options); + let loader; let loadingBbox; let bboxTimeout; - if (options?.strategy === 'bbox') { - loadingBbox = () => { - if (bboxTimeout) { - clearTimeout(bboxTimeout); - bboxTimeout = undefined; - } - bboxTimeout = setTimeout(() => { - const viewRectangle = map.camera.computeViewRectangle(); - const cameraPitch = Math.abs(Cesium.Math.toDegrees(map.camera.pitch)); - if (viewRectangle && cameraPitch > 60) { - loader([ - Cesium.Math.toDegrees(viewRectangle.west), - Cesium.Math.toDegrees(viewRectangle.south), - Cesium.Math.toDegrees(viewRectangle.east), - Cesium.Math.toDegrees(viewRectangle.north) - ]) - .then(({ data: collection }) => { - styledFeatures.setFeatures(collection.features); - layerToGeoStylerStyle(options) - .then((style) => { - getStyle(applyDefaultStyleToVectorLayer({ - ...options, - features: collection.features, - style - }), 'cesium') - .then((styleFunc) => { - styledFeatures.setStyleFunction(styleFunc); - }); - }); - }); + + const add = () => { + loader = createLoader(options); + if (options?.strategy === 'bbox') { + loadingBbox = () => { + if (bboxTimeout) { + clearTimeout(bboxTimeout); + bboxTimeout = undefined; } - }, 300); - }; - map.camera.moveEnd.addEventListener(loadingBbox); - } else { - loader() - .then(({ data: collection }) => { - styledFeatures.setFeatures(collection.features); - layerToGeoStylerStyle(options) - .then((style) => { - getStyle(applyDefaultStyleToVectorLayer({ - ...options, - features: collection.features, - style - }), 'cesium') - .then((styleFunc) => { - styledFeatures.setStyleFunction(styleFunc); + bboxTimeout = setTimeout(() => { + const viewRectangle = map.camera.computeViewRectangle(); + const cameraPitch = Math.abs(Cesium.Math.toDegrees(map.camera.pitch)); + if (viewRectangle && cameraPitch > 60) { + loader([ + Cesium.Math.toDegrees(viewRectangle.west), + Cesium.Math.toDegrees(viewRectangle.south), + Cesium.Math.toDegrees(viewRectangle.east), + Cesium.Math.toDegrees(viewRectangle.north) + ]) + .then(({ data: collection }) => { + styledFeatures.setFeatures(collection.features); + layerToGeoStylerStyle(options) + .then((style) => { + getStyle(applyDefaultStyleToVectorLayer({ + ...options, + features: collection.features, + style + }), 'cesium') + .then((styleFunc) => { + styledFeatures.setStyleFunction(styleFunc); + }); + }); }); - }); - }); - } + } + }, 300); + }; + map.camera.moveEnd.addEventListener(loadingBbox); + } else { + loader() + .then(({ data: collection }) => { + styledFeatures.setFeatures(collection.features); + layerToGeoStylerStyle(options) + .then((style) => { + getStyle(applyDefaultStyleToVectorLayer({ + ...options, + features: collection.features, + style + }), 'cesium') + .then((styleFunc) => { + styledFeatures.setStyleFunction(styleFunc); + }); + }); + }); + } + }; return { detached: true, styledFeatures, + add, remove: () => { if (styledFeatures) { styledFeatures.destroy(); @@ -143,7 +147,7 @@ const createLayer = (options, map) => { Layers.registerType('wfs', { create: createLayer, update: (layer, newOptions, oldOptions, map) => { - if (needsReload(oldOptions, newOptions)) { + if (needsReload(oldOptions, newOptions) || oldOptions.forceProxy !== newOptions.forceProxy) { return createLayer(newOptions, map); } if (layer?.styledFeatures && !isEqual(newOptions.style, oldOptions.style)) { diff --git a/web/client/components/map/cesium/plugins/WMSLayer.js b/web/client/components/map/cesium/plugins/WMSLayer.js index 7175c5f798..d990b8b3a1 100644 --- a/web/client/components/map/cesium/plugins/WMSLayer.js +++ b/web/client/components/map/cesium/plugins/WMSLayer.js @@ -50,7 +50,7 @@ const updateLayer = (layer, newOptions, oldOptions) => { if (newParameters.length > 0 || newOptions.securityToken !== oldOptions.securityToken || !isEqual(newOptions.layerFilter, oldOptions.layerFilter) || - newOptions.tileSize !== oldOptions.tileSize) { + newOptions.tileSize !== oldOptions.tileSize || newOptions.forceProxy !== oldOptions.forceProxy) { return createLayer(newOptions); } return null; diff --git a/web/client/components/map/cesium/plugins/WMTSLayer.js b/web/client/components/map/cesium/plugins/WMTSLayer.js index 040ac8b906..78c867ec96 100644 --- a/web/client/components/map/cesium/plugins/WMTSLayer.js +++ b/web/client/components/map/cesium/plugins/WMTSLayer.js @@ -10,8 +10,7 @@ import Layers from '../../../../utils/cesium/Layers'; import * as Cesium from 'cesium'; import ConfigUtils from '../../../../utils/ConfigUtils'; import { - getProxyUrl, - needProxy + getProxyUrl } from '../../../../utils/ProxyUtils'; import * as WMTSUtils from '../../../../utils/WMTSUtils'; import { creditsToAttribution, getAuthenticationParam, getURLs } from '../../../../utils/LayersUtils'; @@ -110,7 +109,7 @@ function wmtsToCesiumOptions(_options) { let proxyUrl = ConfigUtils.getProxyUrl({}); let proxy; if (proxyUrl) { - proxy = needProxy(options.url) && proxyUrl; + proxy = options.forceProxy; } const isValid = isValidTile(options.matrixIds && options.matrixIds[tileMatrixSetID]); const queryParametersString = urlParser.format({ query: {...getAuthenticationParam(options)}}); @@ -161,7 +160,7 @@ const createLayer = options => { const updateLayer = (layer, newOptions, oldOptions) => { if (newOptions.securityToken !== oldOptions.securityToken || oldOptions.format !== newOptions.format - || oldOptions.credits !== newOptions.credits) { + || oldOptions.credits !== newOptions.credits || newOptions.forceProxy !== oldOptions.forceProxy) { return createLayer(newOptions); } return null; diff --git a/web/client/libs/__tests__/ajax-test.js b/web/client/libs/__tests__/ajax-test.js index 13661a6e23..d135845b9b 100644 --- a/web/client/libs/__tests__/ajax-test.js +++ b/web/client/libs/__tests__/ajax-test.js @@ -470,20 +470,5 @@ describe('Tests ajax library', () => { }); }); - it('revert to proxy if autoDetectCORS is true but CORS is not enabled on server', (done) => { - mockAxios = new MockAdapter(axios); - axios.get('http://testcors/', { - timeout: 1, - proxyUrl: { - url: '/proxy/?url=', - useCORS: [], - autoDetectCORS: true - } - }).catch((response) => { - expect(response.config).toExist(); - expect(response.config.url).toExist(); - expect(response.config.url).toContain('proxy/?url='); - done(); - }); - }); + }); diff --git a/web/client/libs/ajax.js b/web/client/libs/ajax.js index 02acb28c1b..3a4e0fa07a 100644 --- a/web/client/libs/ajax.js +++ b/web/client/libs/ajax.js @@ -6,18 +6,22 @@ * LICENSE file in the root directory of this source tree. */ -const axios = require('axios'); -const combineURLs = require('axios/lib/helpers/combineURLs'); -const url = require('url'); -const ConfigUtils = require('../utils/ConfigUtils').default; -const {isAuthenticationActivated, getAuthenticationRule, getToken, - getBasicAuthHeader} = require('../utils/SecurityUtils'); +import axios from 'axios'; +import combineURLs from 'axios/lib/helpers/combineURLs'; +import ConfigUtils from '../utils/ConfigUtils'; +import { + isAuthenticationActivated, + getAuthenticationRule, + getToken, + getBasicAuthHeader +} from '../utils/SecurityUtils'; -const assign = require('object-assign'); -const isObject = require('lodash/isObject'); -const omitBy = require('lodash/omitBy'); -const isNil = require('lodash/isNil'); -const urlUtil = require('url'); +import assign from 'object-assign'; +import isObject from 'lodash/isObject'; +import omitBy from 'lodash/omitBy'; +import isNil from 'lodash/isNil'; +import urlUtil from 'url'; +import { getProxyCacheByUrl, setProxyCacheByUrl } from '../api/CORS'; /** * Internal helper that adds an extra paramater to an axios configuration. @@ -36,8 +40,6 @@ function addHeaderToAxiosConfig(axiosConfig, headerName, headerValue) { axiosConfig.headers = assign({}, axiosConfig.headers, {[headerName]: headerValue}); } -const corsDisabled = []; - /** * Internal helper that will add to the axios config object the correct * authentication method based on the request URL. @@ -98,11 +100,9 @@ function addAuthenticationToAxios(axiosConfig) { } } -axios.interceptors.request.use(config => { - var uri = config.url || ''; +const checkSameOrigin = (uri) => { var sameOrigin = !(uri.indexOf("http") === 0); var urlParts = !sameOrigin && uri.match(/([^:]*:)\/\/([^:]*:?[^@]*@)?([^:\/\?]*):?([^\/\?]*)/); - addAuthenticationToAxios(config); if (urlParts) { let location = window.location; sameOrigin = @@ -115,19 +115,24 @@ axios.interceptors.request.use(config => { lPort = lPort === "" ? defaultPort + "" : lPort + ""; sameOrigin = sameOrigin && uPort === lPort; } + return sameOrigin; +}; + +axios.interceptors.request.use(config => { + addAuthenticationToAxios(config); + const uri = config.url || ''; + const sameOrigin = checkSameOrigin(uri); if (!sameOrigin) { let proxyUrl = ConfigUtils.getProxyUrl(config); if (proxyUrl) { let useCORS = []; - let autoDetectCORS = false; if (isObject(proxyUrl)) { useCORS = proxyUrl.useCORS || []; - autoDetectCORS = proxyUrl.autoDetectCORS || false; proxyUrl = proxyUrl.url; } - const isCORS = useCORS.reduce((found, current) => found || uri.indexOf(current) === 0, false); - const cannotUseCORS = corsDisabled.reduce((found, current) => found || uri.indexOf(current) === 0, false); - if (!isCORS && (!autoDetectCORS || cannotUseCORS)) { + const isCORS = useCORS.some((current) => uri.indexOf(current) === 0); + const proxyNeeded = getProxyCacheByUrl(uri); + if (!isCORS && proxyNeeded) { const parsedUri = urlUtil.parse(uri, true, true); const params = omitBy(config.params, isNil); config.url = proxyUrl + encodeURIComponent( @@ -139,8 +144,9 @@ axios.interceptors.request.use(config => { ) ); config.params = undefined; - } else if (autoDetectCORS) { - config.autoDetectCORS = true; + } + if (isCORS && proxyNeeded === undefined) { + setProxyCacheByUrl(uri, false); } } } @@ -148,17 +154,17 @@ axios.interceptors.request.use(config => { }); axios.interceptors.response.use(response => response, (error) => { - if (error.config && error.config.autoDetectCORS) { - const urlParts = url.parse(error.config.url); - const baseUrl = urlParts.protocol + "//" + urlParts.host + urlParts.pathname; - if (corsDisabled.indexOf(baseUrl) === -1) { - corsDisabled.push(baseUrl); + let proxyUrl = ConfigUtils.getProxyUrl(); + const sameOrigin = checkSameOrigin(error.config.url || ''); + if (error.config && !error.config.url.includes(proxyUrl.url) && !sameOrigin) { + if (getProxyCacheByUrl(error.config.url) === undefined && typeof error.response === 'undefined') { + setProxyCacheByUrl(error.config.url, true); return new Promise((resolve, reject) => { - axios({ ...error.config, autoDetectCORS: false}).then(resolve).catch(reject); + axios({ ...error.config }).then(resolve).catch(reject); }); } } return Promise.reject(error.response ? {...error.response, originalError: error} : error); }); -module.exports = axios; +export default axios; diff --git a/web/client/observables/wps/__tests__/execute-test.js b/web/client/observables/wps/__tests__/execute-test.js index b6515231f2..3325f2b8cd 100644 --- a/web/client/observables/wps/__tests__/execute-test.js +++ b/web/client/observables/wps/__tests__/execute-test.js @@ -169,7 +169,8 @@ describe('WPS execute tests', () => { mockAxios.onPost().reply(200, responseWithProcessAccepted, {'content-type': 'application/xml'}); mockAxios.onGet().reply((config) => { - const expectedUrl = '/mapstore/proxy/?url=http%3A%2F%2Ftestserver%2F%3Fservice%3DWPS%26version%3D1.0.0%26REQUEST%3DGetExecutionStatus%26executionId%3D0c596a4d-7ddb-4a4e-bf35-4a64b47ee0d3'; + + const expectedUrl = 'http://testserver/?service=WPS&version=1.0.0&REQUEST=GetExecutionStatus&executionId=0c596a4d-7ddb-4a4e-bf35-4a64b47ee0d3'; const responseData = statusResponses.shift(); try { diff --git a/web/client/translations/data.da-DK.json b/web/client/translations/data.da-DK.json index 264a792164..b346ec9d1c 100644 --- a/web/client/translations/data.da-DK.json +++ b/web/client/translations/data.da-DK.json @@ -1500,7 +1500,7 @@ "showPreview": "Show preview", "advancedSettings": "Advanced settings", "templateMetadataAvailable": "Metadata available from Dublin Core format: abstract, boundingBox, contributor, creator, description, format, identifier, references, rights, source, subject, temporal, title, type, uri", - "invalidUrlHttpProtocol": "This catalog cannot be added to the available ones because it uses an http protocol. Please provide a catalog url that uses https protocol", + "invalidUrlHttpProtocol": "The specified URL is not secure as it uses HTTP. An URL using HTTPs protocol should be provided. Using HTTP, requests will be handled automatically to prevent browser security issues by passing through the proxy.", "notification": { "errorTitle": "Error", "errorSearchingRecords": "Some records have not been found: {records} Please check the query URL and its parameters.", diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 93e34246d9..e93be99cda 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -1669,7 +1669,7 @@ "showPreview": "Vorschau zeigen", "advancedSettings": "Erweiterte Einstellungen", "templateMetadataAvailable": "Metadaten im Dublin Core-Format verfügbar: abstract, boundingBox, contributor, creator, description, format, identifier, references, rights, source, subject, temporal, title, type, uri", - "invalidUrlHttpProtocol": "Dieser Katalog kann nicht zu den verfügbaren hinzugefügt werden, da er ein http-Protokoll verwendet. Bitte geben Sie eine Katalog-URL an, die das https-Protokoll verwendet.", + "invalidUrlHttpProtocol": "Die angegebene URL ist nicht sicher, da sie HTTP verwendet. Eine URL, die das HTTPS-Protokoll verwendet, sollte bereitgestellt werden. Bei der Verwendung von HTTP werden Anfragen automatisch verarbeitet, um Browser-Sicherheitsprobleme zu verhindern, indem sie über den Proxy geleitet werden", "invalidArrayUsageForUrl": "Die Katalog-URL konnte nicht korrekt gelesen werden", "notification": { "errorTitle": "Error", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 142808850b..a7548354d7 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -1630,7 +1630,7 @@ "showPreview": "Show preview", "advancedSettings": "Advanced settings", "templateMetadataAvailable": "Metadata available from Dublin Core format: abstract, boundingBox, contributor, creator, description, format, identifier, references, rights, source, subject, temporal, title, type, uri", - "invalidUrlHttpProtocol": "This catalog cannot be added to the available ones because it uses an http protocol. Please provide a catalog url that uses https protocol", + "invalidUrlHttpProtocol": "The specified URL is not secure as it uses HTTP. An URL using HTTPs protocol should be provided. Using HTTP, requests will be handled automatically to prevent browser security issues by passing through the proxy", "invalidArrayUsageForUrl": "The catalog URL could not be parsed correctly", "notification": { "errorTitle": "Error", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 8cf00a2f41..5f1c659d7f 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -1631,7 +1631,7 @@ "showPreview": "Mostrar vista previa", "advancedSettings": "Ajustes avanzados", "templateMetadataAvailable": "Metadatos disponibles en formato Dublin Core: abstract, boundingBox, contributor, creator, description, format, identifier, references, rights, source, subject, temporal, title, type, uri", - "invalidUrlHttpProtocol": "Este catálogo no se puede agregar a los disponibles porque usa un protocolo http. Proporcione una URL del catálogo que utilice el protocolo https", + "invalidUrlHttpProtocol": "La URL especificada no es segura ya que utiliza HTTP. Se debe proporcionar una URL que utilice el protocolo HTTPS. Al utilizar HTTP, las solicitudes se manejarán automáticamente para prevenir problemas de seguridad del navegador al pasar a través del proxy", "invalidArrayUsageForUrl": "La URL del catálogo no se pudo leer correctamente.", "notification": { "errorTitle": "Error", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index dd4c09fbb2..83d52272fa 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -1631,7 +1631,7 @@ "showPreview": "Afficher l'aperçu", "advancedSettings": "Réglages avancés", "templateMetadataAvailable": "Métadonnées disponibles pour le format Dublin Core : abstract, boundingBox, contributor, creator, description, format, identifier, references, rights, source, subject, temporal, title, type, uri", - "invalidUrlHttpProtocol": "Ce catalogue ne peut pas être ajouté à ceux disponibles car il utilise un protocole http. Veuillez fournir une URL de catalogue qui utilise le protocole https.", + "invalidUrlHttpProtocol": "L'URL spécifié n'est pas sécurisé car il utilise HTTP. Une URL utilisant le protocole HTTPS doit être fournie. En utilisant HTTP, les demandes seront traitées automatiquement pour éviter les problèmes de sécurité du navigateur en passant par le proxy", "invalidArrayUsageForUrl": "L'URL du catalogue n'a pas pu être lue correctement", "notification": { "errorTitle": "Erreur", diff --git a/web/client/translations/data.is-IS.json b/web/client/translations/data.is-IS.json index 7fae728924..e074081d78 100644 --- a/web/client/translations/data.is-IS.json +++ b/web/client/translations/data.is-IS.json @@ -1504,7 +1504,7 @@ "showPreview": "Show preview", "advancedSettings": "Advanced settings", "templateMetadataAvailable": "Metadata available from Dublin Core format: abstract, boundingBox, contributor, creator, description, format, identifier, references, rights, source, subject, temporal, title, type, uri", - "invalidUrlHttpProtocol": "This catalog cannot be added to the available ones because it uses an http protocol. Please provide a catalog url that uses https protocol", + "invalidUrlHttpProtocol": "Tilgreindur vefslóð er ekki örugg þar sem hún notar HTTP. Vefslóð sem notar HTTPS-protokollinn ætti að vera veitt. Með því að nota HTTP verða beiðnir sjálfkrafa unnar til að koma í veg fyrir öryggisvandamál vafrans með því að fara í gegnum proxy", "notification": { "errorTitle": "Error", "errorSearchingRecords": "Some records have not been found: {records} Please check the query URL and its parameters.", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 18064a3674..d7d467a089 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -1630,7 +1630,7 @@ "showPreview": "Mostra la preview", "advancedSettings": "Impostazioni avanzate", "templateMetadataAvailable": "Metadati disponibili del formato Dublin Core: abstract, boundingBox, contributor, creator, description, format, identifier, references, rights, source, subject, temporal, title, type, uri", - "invalidUrlHttpProtocol": "Questo catalogo non può essere aggiunto a quelli disponibili perché utilizza un protocollo http. Fornisci un URL di catalogo che utilizzi il protocollo https", + "invalidUrlHttpProtocol": "L'URL specificato non è sicuro poiché utilizza HTTP. È necessario fornire un URL che utilizzi il protocollo HTTPS. Utilizzando HTTP, le richieste verranno gestite automaticamente per prevenire problemi di sicurezza del browser passando attraverso il proxy", "invalidArrayUsageForUrl": "Non è stato possibile leggere correttamente l'URL del catalogo", "notification": { "errorTitle": "Errore", diff --git a/web/client/translations/data.nl-NL.json b/web/client/translations/data.nl-NL.json index 5b5e48d667..ff83012e7c 100644 --- a/web/client/translations/data.nl-NL.json +++ b/web/client/translations/data.nl-NL.json @@ -1624,7 +1624,7 @@ "showPreview": "Voorbeeld weergeven", "advancedSettings": "Geavanceerde instellingen", "templateMetadataAvailable": "Metadata beschikbaar in Dublin Core-formaat: abstract, boundingBox, bijdrager, maker, beschrijving, formaat, identifier, referenties, rechten, bron, onderwerp, tijdelijk, titel, type, uri", - "invalidUrlHttpProtocol": "Catalogus kan niet toegevoegd worden omdat het HTTP protocol gebruikt wordt. Gelieve een URL op te geven die HTTPS gebruikt", + "invalidUrlHttpProtocol": "De opgegeven URL is niet veilig omdat deze HTTP gebruikt. Er moet een URL worden opgegeven die het HTTPS-protocol gebruikt. Bij gebruik van HTTP worden verzoeken automatisch verwerkt om browserbeveiligingsproblemen te voorkomen door via de proxy te gaan", "notification": { "errorTitle": "Fout", "errorSearchingRecords": "Sommige records zijn niet gevonden: {records} Controleer de query-URL en de bijbehorende parameters.", diff --git a/web/client/translations/data.sk-SK.json b/web/client/translations/data.sk-SK.json index 658c7f8e63..accbba40e7 100644 --- a/web/client/translations/data.sk-SK.json +++ b/web/client/translations/data.sk-SK.json @@ -1435,7 +1435,7 @@ "showPreview": "Zobraziť náhľad", "advancedSettings": "Pokročilé nastavenia", "templateMetadataAvailable": "Metadata available from Dublin Core format: abstract, boundingBox, contributor, creator, description, format, identifier, references, rights, source, subject, temporal, title, type, uri", - "invalidUrlHttpProtocol": "Tento katalóg nie je možné pridať k dostupným, pretože používa protokol HTTP. Zadajte katalógovú adresu URL, ktorá používa protokol https", + "invalidUrlHttpProtocol": "Špecifikovaná URL adresa nie je bezpečná, pretože používa HTTP. Mala by sa poskytnúť URL adresa používajúca HTTPS protokol. Pri použití HTTP sa požiadavky automaticky spracujú, aby sa predišlo problémom so zabezpečením prehliadača prechádzaním cez proxy", "notification": { "errorTitle": "Chyba", "errorSearchingRecords": "Some records have not been found: {records} Please check the query URL and its parameters.", diff --git a/web/client/translations/data.sv-SE.json b/web/client/translations/data.sv-SE.json index c77073f50b..f2e3dfde27 100644 --- a/web/client/translations/data.sv-SE.json +++ b/web/client/translations/data.sv-SE.json @@ -1414,7 +1414,7 @@ "showPreview": "Visa förhandsgranskning", "advancedSettings": "Avancerade inställningar", "templateMetadataAvailable": "Metadata tillgänglig från Dublin Core -format: abstract, boundingBox, bidragsgivare, skapare, beskrivning, format, identifierare, referenser, rättigheter, källa, ämne, tidsmässig, titel, typ, uri", - "invalidUrlHttpProtocol": "Denna katalog kan inte läggas till i de tillgängliga eftersom den använder ett http -protokoll. Ange en katalogurl som använder https -protokoll", + "invalidUrlHttpProtocol": "Den angivna URL:en är inte säker eftersom den använder HTTP. En URL som använder HTTPS-protokol bör tillhandahållas. När HTTP används kommer förfrågningar automatiskt att hanteras för att förhindra säkerhetsproblem i webbläsaren genom att gå via proxy", "notification": { "errorTitle": "Fel", "errorSearchingRecords": "Vissa poster har inte hittats: {records} Kontrollera frågeadressen och dess parametrar.", diff --git a/web/client/utils/ConfigUtils.js b/web/client/utils/ConfigUtils.js index d17dc78cd0..fad8a2dd03 100644 --- a/web/client/utils/ConfigUtils.js +++ b/web/client/utils/ConfigUtils.js @@ -361,7 +361,7 @@ export const mergeConfigs = function(baseConfig, mapConfig) { return baseConfig; }; export const getProxyUrl = function(config) { - return config.proxyUrl ? config.proxyUrl : defaultConfig.proxyUrl; + return config?.proxyUrl ? config.proxyUrl : defaultConfig.proxyUrl; }; export const getProxiedUrl = function(uri, config = {}) { diff --git a/web/client/utils/cesium/WMSUtils.js b/web/client/utils/cesium/WMSUtils.js index 9624b3decb..d6a5274e42 100644 --- a/web/client/utils/cesium/WMSUtils.js +++ b/web/client/utils/cesium/WMSUtils.js @@ -9,7 +9,7 @@ import * as Cesium from 'cesium'; import { isArray } from 'lodash'; import { addAuthenticationToSLD, getAuthenticationHeaders } from "../SecurityUtils"; -import { getProxyUrl, needProxy } from "../ProxyUtils"; +import { getProxyUrl } from "../ProxyUtils"; import ConfigUtils from "../ConfigUtils"; import { creditsToAttribution, getAuthenticationParam, getURLs, getWMSVendorParams } from "../LayersUtils"; import { isVectorFormat } from '../VectorTileUtils'; @@ -57,7 +57,7 @@ export const getProxy = (options) => { let proxyUrl = ConfigUtils.getProxyUrl({}); let proxy; if (proxyUrl) { - proxy = options.noCors || needProxy(options.url); + proxy = options.noCors || options.forceProxy; } return proxy ? new WMSProxy(proxyUrl) : new NoProxy(); }; diff --git a/web/client/utils/cesium/__tests__/WMSUtils-test.js b/web/client/utils/cesium/__tests__/WMSUtils-test.js index 073f91235b..89b1e4544f 100644 --- a/web/client/utils/cesium/__tests__/WMSUtils-test.js +++ b/web/client/utils/cesium/__tests__/WMSUtils-test.js @@ -28,7 +28,7 @@ const testLayerConfig = { describe('Test the WMSUtil for Cesium', () => { it('wmsToCesiumOptionsBIL with proxy', () => { - let config = wmsToCesiumOptionsBIL(testLayerConfig); + let config = wmsToCesiumOptionsBIL({...testLayerConfig, forceProxy: true}); expect(config.url).toBe(testLayerConfig.url); expect(config.proxy.getURL("test")).toBe("/mapstore/proxy/?url=test"); expect(config.layerName).toBe(testLayerConfig.name); diff --git a/web/client/utils/mapinfo/__tests__/arcgis-test.js b/web/client/utils/mapinfo/__tests__/arcgis-test.js index 51f9ddebae..8b84aeebc2 100644 --- a/web/client/utils/mapinfo/__tests__/arcgis-test.js +++ b/web/client/utils/mapinfo/__tests__/arcgis-test.js @@ -77,9 +77,9 @@ describe('mapinfo arcgis utils', () => { ]; mockAxios.onGet().reply((req) => { try { - const parts = req.url.split('?url='); - expect(decodeURIComponent(parts[parts.length - 1])) - .toBe('https://test.url/0/query?f=json&geometry=-76.69000174947232%2C34.669673599384076%2C-76.33843924947232%2C34.95830926327919&inSR=4326&outSR=4326&outFields=*'); + expect(req.url).toBe('https://test.url/0/query'); + expect(req.params.geometry).toBe('-76.69000174947232,34.669673599384076,-76.33843924947232,34.95830926327919'); + } catch (e) { done(e); } diff --git a/web/client/utils/openlayers/WMSUtils.js b/web/client/utils/openlayers/WMSUtils.js index 15b1c0b2e5..deb2f4e18d 100644 --- a/web/client/utils/openlayers/WMSUtils.js +++ b/web/client/utils/openlayers/WMSUtils.js @@ -19,6 +19,21 @@ import { getResolutionsForProjection } from '../MapUtils'; import { generateEnvString } from '../LayerLocalizationUtils'; import { getTileGridFromLayerOptions } from '../WMSUtils'; + +function hasHttpProtocol(givenUrl = '') { + if (window?.location?.protocol === 'https:') { + if (givenUrl.indexOf('http') === 0) { + const givenUrlObject = new URL(givenUrl); + return givenUrlObject.protocol === 'http:'; + } + if (givenUrl.indexOf('/') === 0) { + return false; + } + return true; + } + return false; +} + /** * Check source and apply proxy * when `forceProxy` is set on layer options @@ -27,8 +42,9 @@ import { getTileGridFromLayerOptions } from '../WMSUtils'; * @returns {string} */ export const proxySource = (forceProxy, src) => { + const _forceProxy = forceProxy || hasHttpProtocol(src); let newSrc = src; - if (forceProxy && needProxy(src)) { + if (_forceProxy && needProxy(src)) { let proxyUrl = getProxyUrl(); newSrc = proxyUrl + encodeURIComponent(src); }