From 9cf1dec6a9a08ce2c7e204eb869fd5a5070565e6 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 8 Jul 2020 09:24:34 -0400 Subject: [PATCH] Load configuration from EMS-metadata in region-maps (#70888) --- .../__tests__/map/ems_mocks/sample_files.json | 42 ++++++++++++ .../__tests__/map/ems_mocks/sample_tiles.json | 2 +- .../public/map/service_settings.js | 66 +++++++++++++------ .../public/map/service_settings.test.js | 19 +++++- .../public/region_map_visualization.js | 61 ++++++++++++----- 5 files changed, 151 insertions(+), 39 deletions(-) diff --git a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json index cdbed7fa063676..470544cf35b30f 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json +++ b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json @@ -406,6 +406,48 @@ "zh-tw": "國家" } }, + { + "layer_id": "world_countries_with_compromised_attribution", + "created_at": "2017-04-26T17:12:15.978370", + "attribution": [ + { + "label": { + "en": "
Made with NaturalEarth
" + }, + "url": { + "en": "http://www.naturalearthdata.com/about/terms-of-use" + } + }, + { + "label": { + "en": "Elastic Maps Service" + }, + "url": { + "en": "javascript:alert('foobar')" + } + } + ], + "formats": [ + { + "type": "geojson", + "url": "/files/world_countries_v1.geo.json", + "legacy_default": true + } + ], + "fields": [ + { + "type": "id", + "id": "iso2", + "label": { + "en": "ISO 3166-1 alpha-2 code" + } + } + ], + "legacy_ids": [], + "layer_name": { + "en": "World Countries (compromised)" + } + }, { "layer_id": "australia_states", "created_at": "2018-06-27T23:47:32.202380", diff --git a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json index c038bb411daec8..1bbd94879b70cd 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json +++ b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json @@ -11,7 +11,7 @@ { "label": { "en": "OpenMapTiles" }, "url": { "en": "https://openmaptiles.org" } }, { "label": { "en": "MapTiler" }, "url": { "en": "https://www.maptiler.com" } }, { - "label": { "en": "Elastic Maps Service" }, + "label": { "en": "" }, "url": { "en": "https://www.elastic.co/elastic-maps-service" } } ], diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index f4f88bd5807d51..ae40b2c92d40e2 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -89,28 +89,31 @@ export class ServiceSettings { }; } + _backfillSettings = (fileLayer) => { + // Older version of Kibana stored EMS state in the URL-params + // Creates object literal with required parameters as key-value pairs + const format = fileLayer.getDefaultFormatType(); + const meta = fileLayer.getDefaultFormatMeta(); + + return { + name: fileLayer.getDisplayName(), + origin: fileLayer.getOrigin(), + id: fileLayer.getId(), + created_at: fileLayer.getCreatedAt(), + attribution: getAttributionString(fileLayer), + fields: fileLayer.getFieldsInLanguage(), + format: format, //legacy: format and meta are split up + meta: meta, //legacy, format and meta are split up + }; + }; + async getFileLayers() { if (!this._mapConfig.includeElasticMapsService) { return []; } const fileLayers = await this._emsClient.getFileLayers(); - return fileLayers.map((fileLayer) => { - //backfill to older settings - const format = fileLayer.getDefaultFormatType(); - const meta = fileLayer.getDefaultFormatMeta(); - - return { - name: fileLayer.getDisplayName(), - origin: fileLayer.getOrigin(), - id: fileLayer.getId(), - created_at: fileLayer.getCreatedAt(), - attribution: fileLayer.getHTMLAttribution(), - fields: fileLayer.getFieldsInLanguage(), - format: format, //legacy: format and meta are split up - meta: meta, //legacy, format and meta are split up - }; - }); + return fileLayers.map(this._backfillSettings); } /** @@ -139,7 +142,7 @@ export class ServiceSettings { id: tmsService.getId(), minZoom: await tmsService.getMinZoom(), maxZoom: await tmsService.getMaxZoom(), - attribution: tmsService.getHTMLAttribution(), + attribution: getAttributionString(tmsService), }; }) ); @@ -159,16 +162,25 @@ export class ServiceSettings { this._emsClient.addQueryParams(additionalQueryParams); } - async getEMSHotLink(fileLayerConfig) { + async getFileLayerFromConfig(fileLayerConfig) { const fileLayers = await this._emsClient.getFileLayers(); - const layer = fileLayers.find((fileLayer) => { + return fileLayers.find((fileLayer) => { const hasIdByName = fileLayer.hasId(fileLayerConfig.name); //legacy const hasIdById = fileLayer.hasId(fileLayerConfig.id); return hasIdByName || hasIdById; }); + } + + async getEMSHotLink(fileLayerConfig) { + const layer = await this.getFileLayerFromConfig(fileLayerConfig); return layer ? layer.getEMSHotLink() : null; } + async loadFileLayerConfig(fileLayerConfig) { + const fileLayer = await this.getFileLayerFromConfig(fileLayerConfig); + return fileLayer ? this._backfillSettings(fileLayer) : null; + } + async _getAttributesForEMSTMSLayer(isDesaturated, isDarkMode) { const tmsServices = await this._emsClient.getTMSServices(); const emsTileLayerId = this._mapConfig.emsTileLayerId; @@ -189,7 +201,7 @@ export class ServiceSettings { url: await tmsService.getUrlTemplate(), minZoom: await tmsService.getMinZoom(), maxZoom: await tmsService.getMaxZoom(), - attribution: await tmsService.getHTMLAttribution(), + attribution: getAttributionString(tmsService), origin: ORIGIN.EMS, }; } @@ -255,3 +267,17 @@ export class ServiceSettings { return await response.json(); } } + +function getAttributionString(emsService) { + const attributions = emsService.getAttributions(); + const attributionSnippets = attributions.map((attribution) => { + const anchorTag = document.createElement('a'); + anchorTag.setAttribute('rel', 'noreferrer noopener'); + if (attribution.url.startsWith('http://') || attribution.url.startsWith('https://')) { + anchorTag.setAttribute('href', attribution.url); + } + anchorTag.textContent = attribution.label; + return anchorTag.outerHTML; + }); + return attributionSnippets.join(' | '); //!!!this is the current convention used in Kibana +} diff --git a/src/plugins/maps_legacy/public/map/service_settings.test.js b/src/plugins/maps_legacy/public/map/service_settings.test.js index 01facdc54137e0..6e416f7fd5c845 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.test.js +++ b/src/plugins/maps_legacy/public/map/service_settings.test.js @@ -98,6 +98,9 @@ describe('service_settings (FKA tile_map test)', function () { expect(attrs.url.includes('{x}')).toEqual(true); expect(attrs.url.includes('{y}')).toEqual(true); expect(attrs.url.includes('{z}')).toEqual(true); + expect(attrs.attribution).toEqual( + 'OpenStreetMap contributors | OpenMapTiles | MapTiler | <iframe id=\'iframe\' style=\'position:fixed;height: 40%;width: 100%;top: 60%;left: 5%;right:5%;border: 0px;background:white;\' src=\'http://256.256.256.256\'></iframe>' + ); const urlObject = url.parse(attrs.url, true); expect(urlObject.hostname).toEqual('tiles.foobar'); @@ -182,7 +185,7 @@ describe('service_settings (FKA tile_map test)', function () { minZoom: 0, maxZoom: 10, attribution: - 'OpenStreetMap contributors | OpenMapTiles | MapTiler | Elastic Maps Service', + 'OpenStreetMap contributors | OpenMapTiles | MapTiler | <iframe id=\'iframe\' style=\'position:fixed;height: 40%;width: 100%;top: 60%;left: 5%;right:5%;border: 0px;background:white;\' src=\'http://256.256.256.256\'></iframe>', subdomains: [], }, ]; @@ -276,7 +279,6 @@ describe('service_settings (FKA tile_map test)', function () { serviceSettings = makeServiceSettings({ includeElasticMapsService: false, }); - // mapConfig.includeElasticMapsService = false; const tilemapServices = await serviceSettings.getTMSServices(); const expected = []; expect(tilemapServices).toEqual(expected); @@ -289,7 +291,7 @@ describe('service_settings (FKA tile_map test)', function () { const serviceSettings = makeServiceSettings(); serviceSettings.setQueryParams({ foo: 'bar' }); const fileLayers = await serviceSettings.getFileLayers(); - expect(fileLayers.length).toEqual(18); + expect(fileLayers.length).toEqual(19); const assertions = fileLayers.map(async function (fileLayer) { expect(fileLayer.origin).toEqual(ORIGIN.EMS); const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer); @@ -343,5 +345,16 @@ describe('service_settings (FKA tile_map test)', function () { const hotlink = await serviceSettings.getEMSHotLink(fileLayers[0]); expect(hotlink).toEqual('?locale=en#file/world_countries'); //url host undefined becuase emsLandingPageUrl is set at kibana-load }); + + it('should sanitize EMS attribution', async () => { + const serviceSettings = makeServiceSettings(); + const fileLayers = await serviceSettings.getFileLayers(); + const fileLayer = fileLayers.find((layer) => { + return layer.id === 'world_countries_with_compromised_attribution'; + }); + expect(fileLayer.attribution).toEqual( + '<div onclick=\'alert(1\')>Made with NaturalEarth</div> | Elastic Maps Service' + ); + }); }); }); diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js index 002d020fcd5683..43959c367558f0 100644 --- a/src/plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -22,9 +22,11 @@ import ChoroplethLayer from './choropleth_layer'; import { getFormatService, getNotifications, getKibanaLegacy } from './kibana_services'; import { truncatedColorMaps } from '../../charts/public'; import { tooltipFormatter } from './tooltip_formatter'; -import { mapTooltipProvider } from '../../maps_legacy/public'; +import { mapTooltipProvider, ORIGIN } from '../../maps_legacy/public'; +import _ from 'lodash'; export function createRegionMapVisualization({ + regionmapsConfig, serviceSettings, uiSettings, BaseMapsVisualization, @@ -60,17 +62,18 @@ export function createRegionMapVisualization({ }); } - if (!this._params.selectedJoinField && this._params.selectedLayer) { - this._params.selectedJoinField = this._params.selectedLayer.fields[0]; + const selectedLayer = await this._loadConfig(this._params.selectedLayer); + if (!this._params.selectedJoinField && selectedLayer) { + this._params.selectedJoinField = selectedLayer.fields[0]; } - if (!this._params.selectedLayer) { + if (!selectedLayer) { return; } this._updateChoroplethLayerForNewMetrics( - this._params.selectedLayer.name, - this._params.selectedLayer.attribution, + selectedLayer.name, + selectedLayer.attribution, this._params.showAllShapes, results ); @@ -90,29 +93,57 @@ export function createRegionMapVisualization({ this._kibanaMap.useUiStateFromVisualization(this._vis); } + async _loadConfig(fileLayerConfig) { + // Load the selected layer from the metadata-service. + // Do not use the selectedLayer from the visState. + // These settings are stored in the URL and can be used to inject dirty display content. + + if ( + fileLayerConfig.isEMS || //Hosted by EMS. Metadata needs to be resolved through EMS + (fileLayerConfig.layerId && fileLayerConfig.layerId.startsWith(`${ORIGIN.EMS}.`)) //fallback for older saved objects + ) { + return await serviceSettings.loadFileLayerConfig(fileLayerConfig); + } + + //Configured in the kibana.yml. Needs to be resolved through the settings. + const configuredLayer = regionmapsConfig.layers.find( + (layer) => layer.name === fileLayerConfig.name + ); + + if (configuredLayer) { + return { + ...configuredLayer, + attribution: _.escape(configuredLayer.attribution ? configuredLayer.attribution : ''), + }; + } + + return null; + } + async _updateParams() { await super._updateParams(); - const visParams = this._params; - if (!visParams.selectedJoinField && visParams.selectedLayer) { - visParams.selectedJoinField = visParams.selectedLayer.fields[0]; + const selectedLayer = await this._loadConfig(this._params.selectedLayer); + + if (!this._params.selectedJoinField && selectedLayer) { + this._params.selectedJoinField = selectedLayer.fields[0]; } - if (!visParams.selectedJoinField || !visParams.selectedLayer) { + if (!this._params.selectedJoinField || !selectedLayer) { return; } this._updateChoroplethLayerForNewProperties( - visParams.selectedLayer.name, - visParams.selectedLayer.attribution, + selectedLayer.name, + selectedLayer.attribution, this._params.showAllShapes ); const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format); - this._choroplethLayer.setJoinField(visParams.selectedJoinField.name); - this._choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema].value); - this._choroplethLayer.setLineWeight(visParams.outlineWeight); + this._choroplethLayer.setJoinField(this._params.selectedJoinField.name); + this._choroplethLayer.setColorRamp(truncatedColorMaps[this._params.colorSchema].value); + this._choroplethLayer.setLineWeight(this._params.outlineWeight); this._choroplethLayer.setTooltipFormatter( this._tooltipFormatter, metricFieldFormatter,