diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js index a49a422042127..8a77f484205e3 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -26,9 +26,6 @@ import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pa import * as visModule from 'ui/vis'; import { ImageComparator } from 'test_utils/image_comparator'; import worldJson from './world.json'; -import EMS_CATALOGUE from '../../../../../ui/public/vis/__tests__/map/ems_mocks/sample_manifest_6.6.json'; -import EMS_FILES from '../../../../../ui/public/vis/__tests__/map/ems_mocks/sample_files_6.6.json'; -import EMS_TILES from '../../../../../ui/public/vis/__tests__/map/ems_mocks/sample_tiles_6.6.json'; import initialPng from './initial.png'; import toiso3Png from './toiso3.png'; @@ -49,6 +46,8 @@ describe('RegionMapsVisualizationTests', function () { let Vis; let indexPattern; let vis; + let serviceSettings; + let loadFileLayerConfig; let imageComparator; @@ -80,7 +79,6 @@ describe('RegionMapsVisualizationTests', function () { beforeEach(ngMock.module('kibana')); - let getManifestStub; beforeEach(ngMock.inject((Private, $injector) => { Vis = Private(visModule.VisProvider); @@ -96,24 +94,24 @@ describe('RegionMapsVisualizationTests', function () { }); }; - const serviceSettings = $injector.get('serviceSettings'); - getManifestStub = serviceSettings.__debugStubManifestCalls(async (url) => { - //simulate network calls - if (url.startsWith('https://foobar')) { - return EMS_CATALOGUE; - } else if (url.startsWith('https://tiles.foobar')) { - return EMS_TILES; - } else if (url.startsWith('https://files.foobar')) { - return EMS_FILES; - } - }); + serviceSettings = $injector.get('serviceSettings'); + loadFileLayerConfig = serviceSettings.loadFileLayerConfig; + serviceSettings.loadFileLayerConfig = async (fl) => { + // Region-maps visualization calls EMS to dynamically load attribution iso grabbing it from visState + // Mock this call to avoid network-roundtrip + return { + attribution: fl.attribution + '_sanitized', + name: fl.name, + }; + }; + })); afterEach(function () { ChoroplethLayer.prototype._makeJsonAjaxCall = _makeJsonAjaxCallOld; - getManifestStub.removeStub(); + loadFileLayerConfig.loadFileLayerConfig = loadFileLayerConfig; }); diff --git a/src/legacy/core_plugins/region_map/public/choropleth_layer.js b/src/legacy/core_plugins/region_map/public/choropleth_layer.js index e059af9ea7f7c..18304a1fb2b87 100644 --- a/src/legacy/core_plugins/region_map/public/choropleth_layer.js +++ b/src/legacy/core_plugins/region_map/public/choropleth_layer.js @@ -78,7 +78,7 @@ export default class ChoroplethLayer extends KibanaMapLayer { } - constructor(name, attribution, format, showAllShapes, meta, layerConfig) { + constructor({ name, attribution, format, showAllShapes, meta, layerConfig }) { super(); @@ -263,7 +263,14 @@ CORS configuration of the server permits requests from the Kibana application on } cloneChoroplethLayerForNewData(name, attribution, format, showAllData, meta, layerConfig) { - const clonedLayer = new ChoroplethLayer(name, attribution, format, showAllData, meta, layerConfig); + const clonedLayer = new ChoroplethLayer({ + name, + attribution, + format, + showAllShapes: showAllData, + meta, + layerConfig } + ); clonedLayer.setJoinField(this._joinField); clonedLayer.setColorRamp(this._colorRamp); clonedLayer.setLineWeight(this._lineWeight); diff --git a/src/legacy/core_plugins/region_map/public/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/region_map_visualization.js index d915639864a6f..a6e9ba48ace55 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/region_map_visualization.js @@ -20,13 +20,14 @@ import 'plugins/kbn_vislib_vis_types/controls/vislib_basic_options'; import _ from 'lodash'; import { BaseMapsVisualizationProvider } from '../../tile_map/public/base_maps_visualization'; +import { ORIGIN } from '../../tile_map/common/origin'; import ChoroplethLayer from './choropleth_layer'; import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps'; import AggResponsePointSeriesTooltipFormatterProvider from './tooltip_formatter'; import 'ui/vis/map/service_settings'; import { toastNotifications } from 'ui/notify'; -export function RegionMapsVisualizationProvider(Private, config, i18n) { +export function RegionMapsVisualizationProvider(Private, config, i18n, regionmapsConfig, serviceSettings) { const tooltipFormatter = Private(AggResponsePointSeriesTooltipFormatterProvider); const BaseMapsVisualization = Private(BaseMapsVisualizationProvider); @@ -61,17 +62,18 @@ export function RegionMapsVisualizationProvider(Private, config, i18n) { }); } - if (!this._vis.params.selectedJoinField && this._vis.params.selectedLayer) { - this._vis.params.selectedJoinField = this._vis.params.selectedLayer.fields[0]; + const selectedLayer = await this._loadConfig(this.vis.params.selectedLayer); + if (!this.vis.params.selectedJoinField && selectedLayer) { + this.vis.params.selectedJoinField = selectedLayer.fields[0]; } - if (!this._vis.params.selectedLayer) { + if (!selectedLayer) { return; } this._updateChoroplethLayerForNewMetrics( - this._vis.params.selectedLayer.name, - this._vis.params.selectedLayer.attribution, + selectedLayer.name, + selectedLayer.attribution, this._vis.params.showAllShapes, results ); @@ -82,27 +84,58 @@ export function RegionMapsVisualizationProvider(Private, config, i18n) { 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) { + return; + } + + 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.vis.params; - if (!visParams.selectedJoinField && visParams.selectedLayer) { - visParams.selectedJoinField = visParams.selectedLayer.fields[0]; + const selectedLayer = await this._loadConfig(this.vis.params.selectedLayer); + + if (!this.vis.params.selectedJoinField && selectedLayer) { + this.vis.params.selectedJoinField = selectedLayer.fields[0]; } - if (!visParams.selectedJoinField || !visParams.selectedLayer) { + if (!this.vis.params.selectedJoinField || !selectedLayer) { return; } this._updateChoroplethLayerForNewProperties( - visParams.selectedLayer.name, - visParams.selectedLayer.attribution, + selectedLayer.name, + selectedLayer.attribution, this._vis.params.showAllShapes ); - this._choroplethLayer.setJoinField(visParams.selectedJoinField.name); - this._choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema].value); - this._choroplethLayer.setLineWeight(visParams.outlineWeight); + this._choroplethLayer.setJoinField(this.vis.params.selectedJoinField.name); + this._choroplethLayer.setColorRamp(truncatedColorMaps[this.vis.params.colorSchema].value); + this._choroplethLayer.setLineWeight(this.vis.params.outlineWeight); this._setTooltipFormatter(); } @@ -136,14 +169,14 @@ export function RegionMapsVisualizationProvider(Private, config, i18n) { this.vis.params.selectedLayer ); } else { - this._choroplethLayer = new ChoroplethLayer( + this._choroplethLayer = new ChoroplethLayer({ name, attribution, - this.vis.params.selectedLayer.format, - showAllData, - this.vis.params.selectedLayer.meta, - this.vis.params.selectedLayer - ); + format: this.vis.params.selectedLayer.format, + showAllShapes: showAllData, + meta: this.vis.params.selectedLayer.meta, + layerConfig: this.vis.params.selectedLayer + }); } this._choroplethLayer.on('select', (event) => { diff --git a/src/ui/public/vis/__tests__/map/ems_client.js b/src/ui/public/vis/__tests__/map/ems_client.js index 267ada2a8265f..3bbffaf21d896 100644 --- a/src/ui/public/vis/__tests__/map/ems_client.js +++ b/src/ui/public/vis/__tests__/map/ems_client.js @@ -79,7 +79,7 @@ describe('ems_client', () => { it('.getFileLayers', async () => { const emsClient = getEMSClient(); const layers = await emsClient.getFileLayers(); - expect(layers.length).to.be(18); + expect(layers.length).to.be(19); }); it('.getFileLayers[0]', async () => { diff --git a/src/ui/public/vis/__tests__/map/ems_mocks/sample_files_6.6.json b/src/ui/public/vis/__tests__/map/ems_mocks/sample_files_6.6.json index 3ef17ea35352c..eac50df150955 100644 --- a/src/ui/public/vis/__tests__/map/ems_mocks/sample_files_6.6.json +++ b/src/ui/public/vis/__tests__/map/ems_mocks/sample_files_6.6.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": "
© OpenStreetMap contributors | Elastic Maps Service
' + ); const urlObject = url.parse(mapUrl, true); expect(urlObject.hostname).to.be('tiles-stage.elastic.co'); @@ -240,7 +243,7 @@ describe('service_settings (FKA tilemaptest)', function () { serviceSettings.addQueryParams({ foo: 'bar' }); const fileLayers = await serviceSettings.getFileLayers(); - expect(fileLayers.length).to.be(18); + expect(fileLayers.length).to.be(19); const assertions = fileLayers.map(async function (fileLayer) { expect(fileLayer.origin).to.be(ORIGIN.EMS); @@ -258,7 +261,7 @@ describe('service_settings (FKA tilemaptest)', function () { it('should load manifest (individual props)', async () => { const expected = { - attribution: 'Made with NaturalEarth | Elastic Maps Service', + attribution: 'Made with NaturalEarth | Elastic Maps Service', format: 'geojson', fields: [ { 'type': 'id', 'name': 'iso2', 'description': 'ISO 3166-1 alpha-2 code' }, @@ -295,5 +298,15 @@ describe('service_settings (FKA tilemaptest)', function () { }); + it('should sanitize EMS attribution', async () => { + const fileLayers = await serviceSettings.getFileLayers(); + const fileLayer = fileLayers.find((layer) => { + return layer.id === 'world_countries_with_compromised_attribution'; + }); + expect(fileLayer.attribution).to.eql( + '<div onclick=\'alert(1\')>Made with NaturalEarth</div> | Elastic Maps Service' + ); + }); + }); }); diff --git a/src/ui/public/vis/map/service_settings.js b/src/ui/public/vis/map/service_settings.js index b798188846c8d..7e2e8a1435910 100644 --- a/src/ui/public/vis/map/service_settings.js +++ b/src/ui/public/vis/map/service_settings.js @@ -75,6 +75,24 @@ uiModules.get('kibana') }; } + _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 (!mapConfig.includeElasticMapsService) { @@ -82,23 +100,7 @@ uiModules.get('kibana') } 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); } @@ -145,14 +147,23 @@ uiModules.get('kibana') this._emsClient.addQueryParams(additionalQueryParams); } - async getEMSHotLink(fileLayerConfig) { + async getFileLayerFromConfig(fileLayerConfig) { const fileLayers = await this._emsClient.getFileLayers(); - const layer = fileLayers.find(fileLayer => { - const hasIdByName = fileLayer.hasId(fileLayerConfig.name);//legacy - const hasIdById = fileLayer.hasId(fileLayerConfig.id); + return fileLayers.find((fileLayer) => { + const hasIdByName = fileLayer.hasId(fileLayerConfig.name); //legacy + const hasIdById = fileLayer.hasId(fileLayerConfig.id); return hasIdByName || hasIdById; }); - return (layer) ? layer.getEMSHotLink() : null; + } + + async getEMSHotLink(fileLayerConfig) { + const layer = await this.getFileLayerFromConfig(fileLayerConfig); + return layer ? await layer.getEMSHotLink() : null; + } + + async loadFileLayerConfig(fileLayerConfig) { + const fileLayer = await this.getFileLayerFromConfig(fileLayerConfig); + return fileLayer ? this._backfillSettings(fileLayer) : null; } @@ -227,3 +238,18 @@ uiModules.get('kibana') return new ServiceSettings(); }); + + +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 +}