diff --git a/plugin-designs/pplr-data-maps/src/components/MapboxGeocoder.vue b/plugin-designs/pplr-data-maps/src/components/MapboxGeocoder.vue index 96d3148b2..48bc15fd2 100644 --- a/plugin-designs/pplr-data-maps/src/components/MapboxGeocoder.vue +++ b/plugin-designs/pplr-data-maps/src/components/MapboxGeocoder.vue @@ -20,6 +20,7 @@ */ import { ref, watch, onMounted, onBeforeUnmount, computed, inject } from 'vue' import { ensureGeocoder, GEOCODER } from '../utils/pplrData.js' +import { escHtml } from '../utils/pplrMap.js' import { resolveMapProvider } from '@map/composables/useMapProvider.js' import { ensureEngineScript, engineGlobal } from '@map/composables/useMapInstance.js' import { useInstance } from '../composables/usePplrInstance.js' @@ -108,13 +109,13 @@ function renderSuggestion(item) { const local = item?.properties?.__pplr_local const dotColor = local?.color || '' const dot = dotColor - ? `` + ? `` : '' const tag = kind - ? `${kind}` + ? `${escHtml(kind)}` : '' return `
- ${dot}${item.place_name}${tag} + ${dot}${escHtml(item.place_name)}${tag}
` } diff --git a/plugin-designs/pplr-data-maps/src/utils/pplrMap.js b/plugin-designs/pplr-data-maps/src/utils/pplrMap.js index 31d13e164..fa1d63aba 100644 --- a/plugin-designs/pplr-data-maps/src/utils/pplrMap.js +++ b/plugin-designs/pplr-data-maps/src/utils/pplrMap.js @@ -325,10 +325,15 @@ export async function addPGPoints(map, { } /** Standard popup HTML for a PG feature (PG-in-country grain). */ +// Every value interpolated into popup / suggestion HTML goes through this (CRITIC-3, the critic +// fact-check). The data is PPLR's baked JSON and, for place search, the Mapbox geocoding API, and +// setHTML() parses whatever it is handed; escaping costs nothing and closes the door. +export const escHtml = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])) + export function popupForPG(p, extraRows = '') { - const dash = v => (v == null || v === 'None' || v === '' ? '—' : v) + const dash = v => (v == null || v === 'None' || v === '' ? '—' : escHtml(v)) return `
-

${p.name || '(no name)'}

+

${p.name ? escHtml(p.name) : '(no name)'}

diff --git a/plugin-designs/pplr-data-maps/src/views/pplr-coverage.vue b/plugin-designs/pplr-data-maps/src/views/pplr-coverage.vue index 1b60f74f6..5c5aa856f 100644 --- a/plugin-designs/pplr-data-maps/src/views/pplr-coverage.vue +++ b/plugin-designs/pplr-data-maps/src/views/pplr-coverage.vue @@ -12,7 +12,7 @@ import { usePplrT } from '../composables/usePplrT.js' */ import { inject, onMounted, onBeforeUnmount, ref } from 'vue' import { useMapInstance } from '@map/composables/useMapInstance.js' -import { addCountryPolygons, addPGPoints, loadOnce, LOCALES, popupForPG, POPUP_CSS } from '../utils/pplrMap.js' +import { addCountryPolygons, addPGPoints, loadOnce, LOCALES, popupForPG, POPUP_CSS, escHtml } from '../utils/pplrMap.js' import LoadingGlobe from '../components/LoadingGlobe.vue' import DataSourcesDropdown from '../components/DataSourcesDropdown.vue' @@ -101,9 +101,9 @@ onMounted(async () => { map.value.on('click', 'choropleth', e => { const p = e.features[0].properties const html = `
-

${p.CtryNm || p.ctryNmDisp || '—'}${p.GENCalpha3 || p.ISOalpha2 || '?'}

+

${escHtml(p.CtryNm || p.ctryNmDisp || '—')}${escHtml(p.GENCalpha3 || p.ISOalpha2 || '?')}

Country${dash(p.country)} ${dash(p.iso3)}
ROP3${dash(p.rop3)}
- +
PG count${p.pgCount || 0}
PG count${escHtml(p.pgCount || 0)}
Avg locales filled ${p.avgFilled != null ? p.avgFilled.toFixed(1) : '—'} / 17
diff --git a/plugin-designs/pplr-data-maps/src/views/pplr-language-families.vue b/plugin-designs/pplr-data-maps/src/views/pplr-language-families.vue index f7373f9a7..c6d8ed176 100644 --- a/plugin-designs/pplr-data-maps/src/views/pplr-language-families.vue +++ b/plugin-designs/pplr-data-maps/src/views/pplr-language-families.vue @@ -16,7 +16,7 @@ import { inject, onMounted, onBeforeUnmount, ref, computed, watch } from 'vue' import { resolveNamedStyle } from '@map/styles/registry.js' import { resolveMapProvider } from '@map/composables/useMapProvider.js' import { useMapInstance } from '@map/composables/useMapInstance.js' -import { addCountryPolygons, POPUP_CSS, FullscreenToggleControl, makePinIndicator, bindFuzzyPinClick } from '../utils/pplrMap.js' +import { addCountryPolygons, POPUP_CSS, FullscreenToggleControl, makePinIndicator, bindFuzzyPinClick, escHtml } from '../utils/pplrMap.js' import SemanticTreeLegend from '../components/SemanticTreeLegend.vue' import MapboxGeocoder from '../components/MapboxGeocoder.vue' import PinDetailCard from '../components/PinDetailCard.vue' @@ -410,10 +410,10 @@ function setupLayers() { } function popupHtml(p) { - const d = v => (v == null || v === 'None' || v === '' || v === '*') ? '—' : v + const d = v => (v == null || v === 'None' || v === '' || v === '*') ? '—' : escHtml(v) const color = familyColor(p.fam || 'Other') return `
-

${p.name || '—'}${d(p.country)}

+

${d(p.name)}${d(p.country)}

diff --git a/plugin-designs/pplr-data-maps/src/views/pplr-religion.vue b/plugin-designs/pplr-data-maps/src/views/pplr-religion.vue index e972e5d85..715d611c0 100644 --- a/plugin-designs/pplr-data-maps/src/views/pplr-religion.vue +++ b/plugin-designs/pplr-data-maps/src/views/pplr-religion.vue @@ -12,7 +12,7 @@ import { inject, onMounted, onBeforeUnmount, ref, computed, watch } from 'vue' import { resolveNamedStyle } from '@map/styles/registry.js' import { resolveMapProvider } from '@map/composables/useMapProvider.js' import { useMapInstance } from '@map/composables/useMapInstance.js' -import { addCountryPolygons, POPUP_CSS, COUNTRY_FS_URL, COUNTRY_FS_FIELDS, FullscreenToggleControl, makePinIndicator, bindFuzzyPinClick } from '../utils/pplrMap.js' +import { addCountryPolygons, POPUP_CSS, COUNTRY_FS_URL, COUNTRY_FS_FIELDS, FullscreenToggleControl, makePinIndicator, bindFuzzyPinClick, escHtml } from '../utils/pplrMap.js' import MapboxGeocoder from '../components/MapboxGeocoder.vue' import PinDetailCard from '../components/PinDetailCard.vue' import FlatLegend from '../components/FlatLegend.vue' @@ -231,10 +231,10 @@ const detailColor = computed(() => { }) function popupHtml(p) { - const d = v => (v == null || v === 'None' || v === '' || v === '*') ? '—' : v + const d = v => (v == null || v === 'None' || v === '' || v === '*') ? '—' : escHtml(v) const grp = RELIGION_GROUPS.find(g => g.key === p.rg) return `
-

${p.name || '—'}${d(p.country)}

+

${d(p.name)}${d(p.country)}

Language${d(p.language)}
Family${d(p.fam)}
diff --git a/plugin-designs/pplr-data-maps/src/views/pplr-rop-tree.vue b/plugin-designs/pplr-data-maps/src/views/pplr-rop-tree.vue index b8630d609..b20473208 100644 --- a/plugin-designs/pplr-data-maps/src/views/pplr-rop-tree.vue +++ b/plugin-designs/pplr-data-maps/src/views/pplr-rop-tree.vue @@ -12,7 +12,7 @@ import { inject, onMounted, onBeforeUnmount, ref, computed, shallowRef, watch } import { resolveNamedStyle } from '@map/styles/registry.js' import { resolveMapProvider } from '@map/composables/useMapProvider.js' import { useMapInstance } from '@map/composables/useMapInstance.js' -import { addCountryPolygons, setCountryPolygonTheme, POPUP_CSS, COUNTRY_FS_URL, COUNTRY_FS_FIELDS, FullscreenToggleControl, makePinIndicator, bindFuzzyPinClick } from '../utils/pplrMap.js' +import { addCountryPolygons, setCountryPolygonTheme, POPUP_CSS, COUNTRY_FS_URL, COUNTRY_FS_FIELDS, FullscreenToggleControl, makePinIndicator, bindFuzzyPinClick, escHtml } from '../utils/pplrMap.js' import SemanticTreeLegend from '../components/SemanticTreeLegend.vue' import MapboxGeocoder from '../components/MapboxGeocoder.vue' import PinDetailCard from '../components/PinDetailCard.vue' @@ -376,9 +376,9 @@ function setupLayers() { } function popupHtml(p) { - const d = v => (v == null || v === 'None' || v === '' || v === '*') ? '—' : v + const d = v => (v == null || v === 'None' || v === '' || v === '*') ? '—' : escHtml(v) return `
-

${p.name || '—'}${d(p.country)}

+

${d(p.name)}${d(p.country)}

Religion${d(p.religion)}
Language${d(p.language)}
Affinity Bloc${d(p.rop1)}
Cluster${d(p.rop2)}