Merge pull request #1782 from umap-project/fix-caption

fix: ensure tilelayer attribution with smart text is displayed as HMTL
This commit is contained in:
Yohan Boniface 2024-05-01 17:05:24 +02:00 committed by GitHub
commit 713214e258
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 255 additions and 156 deletions

View file

@ -70,7 +70,6 @@ export default function getPurify() {
export function escapeHTML(s) { export function escapeHTML(s) {
s = s ? s.toString() : '' s = s ? s.toString() : ''
s = getPurify().sanitize(s, { s = getPurify().sanitize(s, {
USE_PROFILES: { html: true },
ADD_TAGS: ['iframe'], ADD_TAGS: ['iframe'],
ALLOWED_TAGS: [ ALLOWED_TAGS: [
'h3', 'h3',
@ -86,9 +85,10 @@ export function escapeHTML(s) {
'iframe', 'iframe',
'img', 'img',
'br', 'br',
'span',
], ],
ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'], ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'],
ALLOWED_ATTR: ['href', 'src', 'width', 'height'], ALLOWED_ATTR: ['href', 'src', 'width', 'height', 'style'],
// Added: `geo:` URL scheme as defined in RFC5870: // Added: `geo:` URL scheme as defined in RFC5870:
// https://www.rfc-editor.org/rfc/rfc5870.html // https://www.rfc-editor.org/rfc/rfc5870.html
// The base RegExp comes from: // The base RegExp comes from:

View file

@ -35,27 +35,25 @@ U.AutoComplete = L.Class.extend({
}, },
createInput: function () { createInput: function () {
this.input = L.DomUtil.element( this.input = L.DomUtil.element({
'input', tagName: 'input',
{
type: 'text', type: 'text',
parent: this.el,
placeholder: this.options.placeholder, placeholder: this.options.placeholder,
autocomplete: 'off', autocomplete: 'off',
className: this.options.className, className: this.options.className,
}, })
this.el
)
L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this) L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this)
L.DomEvent.on(this.input, 'keyup', this.onKeyUp, this) L.DomEvent.on(this.input, 'keyup', this.onKeyUp, this)
L.DomEvent.on(this.input, 'blur', this.onBlur, this) L.DomEvent.on(this.input, 'blur', this.onBlur, this)
}, },
createContainer: function () { createContainer: function () {
this.container = L.DomUtil.element( this.container = L.DomUtil.element({
'ul', tagName: 'ul',
{ className: 'umap-autocomplete' }, parent: document.body,
document.body className: 'umap-autocomplete',
) })
}, },
resizeContainer: function () { resizeContainer: function () {
@ -174,8 +172,11 @@ U.AutoComplete = L.Class.extend({
}, },
createResult: function (item) { createResult: function (item) {
const el = L.DomUtil.element('li', {}, this.container) const el = L.DomUtil.element({
el.textContent = item.label tagName: 'li',
parent: this.container,
textContent: item.label,
})
const result = { const result = {
item: item, item: item,
el: el, el: el,
@ -276,15 +277,22 @@ U.AutoComplete.Ajax.SelectMultiple = U.AutoComplete.Ajax.extend({
initSelectedContainer: function () { initSelectedContainer: function () {
return L.DomUtil.after( return L.DomUtil.after(
this.input, this.input,
L.DomUtil.element('ul', { className: 'umap-multiresult' }) L.DomUtil.element({ tagName: 'ul', className: 'umap-multiresult' })
) )
}, },
displaySelected: function (result) { displaySelected: function (result) {
const result_el = L.DomUtil.element('li', {}, this.selected_container) const result_el = L.DomUtil.element({
tagName: 'li',
parent: this.selected_container,
})
result_el.textContent = result.item.label result_el.textContent = result.item.label
const close = L.DomUtil.element('span', { className: 'close' }, result_el) const close = L.DomUtil.element({
close.textContent = '×' tagName: 'span',
parent: result_el,
className: 'close',
textContent: '×',
})
L.DomEvent.on( L.DomEvent.on(
close, close,
'click', 'click',
@ -302,15 +310,22 @@ U.AutoComplete.Ajax.Select = U.AutoComplete.Ajax.extend({
initSelectedContainer: function () { initSelectedContainer: function () {
return L.DomUtil.after( return L.DomUtil.after(
this.input, this.input,
L.DomUtil.element('div', { className: 'umap-singleresult' }) L.DomUtil.element({ tagName: 'div', className: 'umap-singleresult' })
) )
}, },
displaySelected: function (result) { displaySelected: function (result) {
const result_el = L.DomUtil.element('div', {}, this.selected_container) const result_el = L.DomUtil.element({
tagName: 'div',
parent: this.selected_container,
})
result_el.textContent = result.item.label result_el.textContent = result.item.label
const close = L.DomUtil.element('span', { className: 'close' }, result_el) const close = L.DomUtil.element({
close.textContent = '×' tagName: 'span',
parent: result_el,
className: 'close',
textContent: '×',
})
this.input.style.display = 'none' this.input.style.display = 'none'
L.DomEvent.on( L.DomEvent.on(
close, close,

View file

@ -669,20 +669,27 @@ const ControlsMixin = {
L.DomUtil.createTitle(container, this.options.name, 'icon-caption') L.DomUtil.createTitle(container, this.options.name, 'icon-caption')
this.permissions.addOwnerLink('h5', container) this.permissions.addOwnerLink('h5', container)
if (this.options.description) { if (this.options.description) {
const description = L.DomUtil.create('div', 'umap-map-description', container) const description = L.DomUtil.element({
description.innerHTML = U.Utils.toHTML(this.options.description) tagName: 'div',
className: 'umap-map-description',
safeHTML: U.Utils.toHTML(this.options.description),
parent: container,
})
} }
const datalayerContainer = L.DomUtil.create('div', 'datalayer-container', container) const datalayerContainer = L.DomUtil.create('div', 'datalayer-container', container)
this.eachVisibleDataLayer((datalayer) => { this.eachVisibleDataLayer((datalayer) => {
if (!datalayer.options.inCaption) return if (!datalayer.options.inCaption) return
const p = L.DomUtil.create('p', 'datalayer-legend', datalayerContainer), const p = L.DomUtil.create('p', 'datalayer-legend', datalayerContainer),
legend = L.DomUtil.create('span', '', p), legend = L.DomUtil.create('span', '', p),
headline = L.DomUtil.create('strong', '', p), headline = L.DomUtil.create('strong', '', p)
description = L.DomUtil.create('span', '', p)
datalayer.onceLoaded(function () { datalayer.onceLoaded(function () {
datalayer.renderLegend(legend) datalayer.renderLegend(legend)
if (datalayer.options.description) { if (datalayer.options.description) {
description.innerHTML = U.Utils.toHTML(datalayer.options.description) L.DomUtil.element({
tagName: 'span',
parent: p,
safeHTML: U.Utils.toHTML(datalayer.options.description),
})
} }
}) })
datalayer.renderToolbox(headline) datalayer.renderToolbox(headline)
@ -692,12 +699,11 @@ const ControlsMixin = {
credits = L.DomUtil.createFieldset(creditsContainer, L._('Credits')) credits = L.DomUtil.createFieldset(creditsContainer, L._('Credits'))
title = L.DomUtil.add('h5', '', credits, L._('User content credits')) title = L.DomUtil.add('h5', '', credits, L._('User content credits'))
if (this.options.shortCredit || this.options.longCredit) { if (this.options.shortCredit || this.options.longCredit) {
L.DomUtil.add( L.DomUtil.element({
'p', tagName: 'p',
'', parent: credits,
credits, safeHTML: U.Utils.toHTML(this.options.longCredit || this.options.shortCredit),
U.Utils.toHTML(this.options.longCredit || this.options.shortCredit) })
)
} }
if (this.options.licence) { if (this.options.licence) {
const licence = L.DomUtil.add( const licence = L.DomUtil.add(
@ -718,21 +724,26 @@ const ControlsMixin = {
L.DomUtil.create('hr', '', credits) L.DomUtil.create('hr', '', credits)
title = L.DomUtil.create('h5', '', credits) title = L.DomUtil.create('h5', '', credits)
title.textContent = L._('Map background credits') title.textContent = L._('Map background credits')
const tilelayerCredit = L.DomUtil.create('p', '', credits), const tilelayerCredit = L.DomUtil.create('p', '', credits)
name = L.DomUtil.create('strong', '', tilelayerCredit), L.DomUtil.element({
attribution = L.DomUtil.create('span', '', tilelayerCredit) tagName: 'strong',
name.textContent = `${this.selected_tilelayer.options.name} ` parent: tilelayerCredit,
attribution.innerHTML = this.selected_tilelayer.getAttribution() textContent: `${this.selected_tilelayer.options.name} `,
})
L.DomUtil.element({
tagName: 'span',
parent: tilelayerCredit,
safeHTML: this.selected_tilelayer.getAttribution(),
})
L.DomUtil.create('hr', '', credits) L.DomUtil.create('hr', '', credits)
const umapCredit = L.DomUtil.create('p', '', credits), const urls = {
urls = {
leaflet: 'http://leafletjs.com', leaflet: 'http://leafletjs.com',
django: 'https://www.djangoproject.com', django: 'https://www.djangoproject.com',
umap: 'http://wiki.openstreetmap.org/wiki/UMap', umap: 'http://wiki.openstreetmap.org/wiki/UMap',
changelog: 'https://umap-project.readthedocs.io/en/master/changelog/', changelog: 'https://umap-project.readthedocs.io/en/master/changelog/',
version: this.options.umap_version, version: this.options.umap_version,
} }
umapCredit.innerHTML = L._( const creditHTML = L._(
` `
Powered by <a href="{leaflet}">Leaflet</a> and Powered by <a href="{leaflet}">Leaflet</a> and
<a href="{django}">Django</a>, <a href="{django}">Django</a>,
@ -741,6 +752,7 @@ const ControlsMixin = {
`, `,
urls urls
) )
L.DomUtil.element({ tagName: 'p', innerHTML: creditHTML, parent: credits })
this.panel.open({ content: container }) this.panel.open({ content: container })
}, },
@ -1046,16 +1058,16 @@ U.AttributionControl = L.Control.Attribution.extend({
// Use our own container, so we can hide/show on small screens // Use our own container, so we can hide/show on small screens
const credits = this._container.innerHTML const credits = this._container.innerHTML
this._container.innerHTML = '' this._container.innerHTML = ''
const container = L.DomUtil.add( const container = L.DomUtil.create('div', 'attribution-container', this._container)
'div', container.innerHTML = credits
'attribution-container',
this._container,
credits
)
const shortCredit = this._map.getOption('shortCredit'), const shortCredit = this._map.getOption('shortCredit'),
captionMenus = this._map.getOption('captionMenus') captionMenus = this._map.getOption('captionMenus')
if (shortCredit) { if (shortCredit) {
L.DomUtil.add('span', '', container, `${U.Utils.toHTML(shortCredit)}`) L.DomUtil.element({
tagName: 'span',
parent: container,
safeHTML: `${U.Utils.toHTML(shortCredit)}`,
})
} }
if (captionMenus) { if (captionMenus) {
const link = L.DomUtil.add('a', '', container, `${L._('About')}`) const link = L.DomUtil.add('a', '', container, `${L._('About')}`)

View file

@ -118,19 +118,21 @@ L.DomUtil.createLink = (className, container, content, url, target, title) => {
} }
L.DomUtil.createIcon = (parent, className, title, size = 16) => { L.DomUtil.createIcon = (parent, className, title, size = 16) => {
return L.DomUtil.element( return L.DomUtil.element({
'i', tagName: 'i',
{ className: `icon icon-${size} ${className}`, title: title || '' }, parent: parent,
parent className: `icon icon-${size} ${className}`,
) title: title || '',
})
} }
L.DomUtil.createButtonIcon = (parent, className, title, size = 16) => { L.DomUtil.createButtonIcon = (parent, className, title, size = 16) => {
return L.DomUtil.element( return L.DomUtil.element({
'button', tagName: 'button',
{ className: `icon icon-${size} ${className}`, title: title || '' }, parent: parent,
parent className: `icon icon-${size} ${className}`,
) title: title || '',
})
} }
L.DomUtil.createTitle = (parent, text, className, tag = 'h3') => { L.DomUtil.createTitle = (parent, text, className, tag = 'h3') => {
@ -163,8 +165,13 @@ L.DomUtil.classIf = (el, className, bool) => {
else L.DomUtil.removeClass(el, className) else L.DomUtil.removeClass(el, className)
} }
L.DomUtil.element = (what, attrs, parent) => { L.DomUtil.element = ({ tagName, parent, ...attrs }) => {
const el = document.createElement(what) const el = document.createElement(tagName)
if (attrs.innerHTML) {
attrs.innerHTML = U.Utils.escapeHTML(attrs.innerHTML)
} else if (attrs.safeHTML) {
attrs.innerHTML = attrs.safeHTML
}
for (const attr in attrs) { for (const attr in attrs) {
el[attr] = attrs[attr] el[attr] = attrs[attr]
} }

View file

@ -757,12 +757,15 @@ L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({
}, },
buildLabel: function () { buildLabel: function () {
this.label = L.DomUtil.element('legend', {textContent: this.options.label}) this.label = L.DomUtil.element({
tagName: 'legend',
textContent: this.options.label,
})
}, },
buildLi: function (value) { buildLi: function (value) {
const property_li = L.DomUtil.create('li', '', this.ul) const property_li = L.DomUtil.create('li', '', this.ul)
const label = L.DomUtil.add('label', '', property_li) const label = L.DomUtil.create('label', '', property_li)
const input = L.DomUtil.create('input', '', label) const input = L.DomUtil.create('input', '', label)
L.DomUtil.add('span', '', label, value) L.DomUtil.add('span', '', label, value)
@ -800,14 +803,14 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({
build: function () { build: function () {
this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode) this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode)
this.container.appendChild(this.label) this.container.appendChild(this.label)
const {min, max, type} = this.options.criteria const { min, max, type } = this.options.criteria
this.type = type this.type = type
this.inputType = this.getInputType(this.type) this.inputType = this.getInputType(this.type)
const [minLabel, maxLabel] = this.getLabels() const [minLabel, maxLabel] = this.getLabels()
this.minLabel = L.DomUtil.create('label', '', this.container) this.minLabel = L.DomUtil.create('label', '', this.container)
this.minLabel.innerHTML = minLabel this.minLabel.textContent = minLabel
this.minInput = L.DomUtil.create('input', '', this.minLabel) this.minInput = L.DomUtil.create('input', '', this.minLabel)
this.minInput.type = this.inputType this.minInput.type = this.inputType
@ -817,9 +820,8 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({
this.minInput.dataset.value = min this.minInput.dataset.value = min
} }
this.maxLabel = L.DomUtil.create('label', '', this.container) this.maxLabel = L.DomUtil.create('label', '', this.container)
this.maxLabel.innerHTML = maxLabel this.maxLabel.textContent = maxLabel
this.maxInput = L.DomUtil.create('input', '', this.maxLabel) this.maxInput = L.DomUtil.create('input', '', this.maxLabel)
this.maxInput.type = this.inputType this.maxInput.type = this.inputType
@ -834,7 +836,10 @@ L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({
}, },
buildLabel: function () { buildLabel: function () {
this.label = L.DomUtil.element('legend', {textContent: this.options.label}) this.label = L.DomUtil.element({
tagName: 'legend',
textContent: this.options.label,
})
}, },
toJS: function () { toJS: function () {
@ -974,22 +979,23 @@ L.FormBuilder.Range = L.FormBuilder.FloatInput.extend({
}, },
buildHelpText: function () { buildHelpText: function () {
const datalist = L.DomUtil.create(
'datalist',
'umap-field-datalist',
this.getHelpTextParent()
)
datalist.id = `range-${this.options.label || this.name}`
this.input.setAttribute('list', datalist.id)
let options = '' let options = ''
const step = this.options.step || 1, const step = this.options.step || 1
digits = step < 1 ? 1 : 0 const digits = step < 1 ? 1 : 0
const id = `range-${this.options.label || this.name}`
for (let i = this.options.min; i <= this.options.max; i += this.options.step) { for (let i = this.options.min; i <= this.options.max; i += this.options.step) {
options += `<option value="${i.toFixed(digits)}" label="${i.toFixed( options += `<option value="${i.toFixed(digits)}" label="${i.toFixed(
digits digits
)}"></option>` )}"></option>`
} }
datalist.innerHTML = options const datalist = L.DomUtil.element({
tagName: 'datalist',
parent: this.getHelpTextParent(),
className: 'umap-field-datalist',
safeHTML: options,
id: id,
})
this.input.setAttribute('list', id)
L.FormBuilder.Input.prototype.buildHelpText.call(this) L.FormBuilder.Input.prototype.buildHelpText.call(this)
}, },
}) })

View file

@ -15,21 +15,24 @@ U.Importer = L.Class.extend({
this.presetBox = L.DomUtil.create('div', 'formbox', this.container) this.presetBox = L.DomUtil.create('div', 'formbox', this.container)
this.presetSelect = L.DomUtil.create('select', '', this.presetBox) this.presetSelect = L.DomUtil.create('select', '', this.presetBox)
this.fileBox = L.DomUtil.create('div', 'formbox', this.container) this.fileBox = L.DomUtil.create('div', 'formbox', this.container)
this.fileInput = L.DomUtil.element( this.fileInput = L.DomUtil.element({
'input', tagName: 'input',
{ type: 'file', multiple: 'multiple', autofocus: true }, type: 'file',
this.fileBox parent: this.fileBox,
) multiple: 'multiple',
this.urlInput = L.DomUtil.element( autofocus: true,
'input', })
{ type: 'text', placeholder: L._('Provide an URL here') }, this.urlInput = L.DomUtil.element({
this.container tagName: 'input',
) type: 'text',
this.rawInput = L.DomUtil.element( parent: this.container,
'textarea', placeholder: L._('Provide an URL here'),
{ placeholder: L._('Paste your data here') }, })
this.container this.rawInput = L.DomUtil.element({
) tagName: 'textarea',
parent: this.container,
placeholder: L._('Paste your data here'),
})
this.typeLabel = L.DomUtil.add( this.typeLabel = L.DomUtil.add(
'label', 'label',
'', '',
@ -42,33 +45,43 @@ U.Importer = L.Class.extend({
this.container, this.container,
L._('Choose the layer to import in') L._('Choose the layer to import in')
) )
this.clearLabel = L.DomUtil.element( this.clearLabel = L.DomUtil.element({
'label', tagName: 'label',
{ textContent: L._('Replace layer content'), for: 'datalayer-clear-check' }, parent: this.container,
this.container textContent: L._('Replace layer content'),
) for: 'datalayer-clear-check',
this.submitInput = L.DomUtil.element( })
'input', this.submitInput = L.DomUtil.element({
{ type: 'button', value: L._('Import'), className: 'button' }, tagName: 'input',
this.container type: 'button',
) parent: this.container,
value: L._('Import'),
className: 'button',
})
this.map.help.button(this.typeLabel, 'importFormats') this.map.help.button(this.typeLabel, 'importFormats')
this.typeInput = L.DomUtil.element('select', { name: 'format' }, this.typeLabel) this.typeInput = L.DomUtil.element({
this.layerInput = L.DomUtil.element( tagName: 'select',
'select', name: 'format',
{ name: 'datalayer' }, parent: this.typeLabel,
this.layerLabel })
) this.layerInput = L.DomUtil.element({
this.clearFlag = L.DomUtil.element( tagName: 'select',
'input', name: 'datalayer',
{ type: 'checkbox', name: 'clear', id: 'datalayer-clear-check' }, parent: this.layerLabel,
this.clearLabel })
) this.clearFlag = L.DomUtil.element({
L.DomUtil.element( tagName: 'input',
'option', type: 'checkbox',
{ value: '', textContent: L._('Choose the data format') }, name: 'clear',
this.typeInput id: 'datalayer-clear-check',
) parent: this.clearLabel,
})
L.DomUtil.element({
tagName: 'option',
value: '',
textContent: L._('Choose the data format'),
parent: this.typeInput,
})
for (let i = 0; i < this.TYPES.length; i++) { for (let i = 0; i < this.TYPES.length; i++) {
option = L.DomUtil.create('option', '', this.typeInput) option = L.DomUtil.create('option', '', this.typeInput)
option.value = option.textContent = this.TYPES[i] option.value = option.textContent = this.TYPES[i]
@ -119,11 +132,12 @@ U.Importer = L.Class.extend({
option.value = id option.value = id
} }
}) })
L.DomUtil.element( L.DomUtil.element({
'option', tagName: 'option',
{ value: '', textContent: L._('Import in a new layer') }, value: '',
this.layerInput textContent: L._('Import in a new layer'),
) parent: this.layerInput,
})
}) })
}, },

View file

@ -62,9 +62,15 @@ U.MapPermissions = L.Class.extend({
L.DomUtil.createTitle(container, L._('Update permissions'), 'icon-key') L.DomUtil.createTitle(container, L._('Update permissions'), 'icon-key')
if (this.isAnonymousMap()) { if (this.isAnonymousMap()) {
if (this.options.anonymous_edit_url) { if (this.options.anonymous_edit_url) {
const helpText = `${L._('Secret edit link:')}<br>${this.options.anonymous_edit_url const helpText = `${L._('Secret edit link:')}<br>${
this.options.anonymous_edit_url
}` }`
L.DomUtil.add('p', 'help-text', container, helpText) L.DomUtil.element({
tagName: 'p',
className: 'help-text',
innerHTML: helpText,
parent: container,
})
fields.push([ fields.push([
'options.edit_status', 'options.edit_status',
{ {

View file

@ -190,7 +190,11 @@ U.PopupTemplate.Table = U.PopupTemplate.BaseWithTitle.extend({
addRow: function (container, key, value) { addRow: function (container, key, value) {
const tr = L.DomUtil.create('tr', '', container) const tr = L.DomUtil.create('tr', '', container)
L.DomUtil.add('th', '', tr, key) L.DomUtil.add('th', '', tr, key)
L.DomUtil.add('td', '', tr, this.formatRow(key, value)) L.DomUtil.element({
tagName: 'td',
parent: tr,
innerHTML: this.formatRow(key, value),
})
}, },
renderBody: function () { renderBody: function () {
@ -281,11 +285,12 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
} }
} }
if (props.website) { if (props.website) {
L.DomUtil.element( L.DomUtil.element({
'a', tagName: 'a',
{ href: props.website, textContent: props.website }, parent: container,
container href: props.website,
) textContent: props.website,
})
} }
const phone = props.phone || props['contact:phone'] const phone = props.phone || props['contact:phone']
if (phone) { if (phone) {
@ -293,7 +298,7 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
'div', 'div',
'', '',
container, container,
L.DomUtil.element('a', { href: `tel:${phone}`, textContent: phone }) L.DomUtil.element({ tagName: 'a', href: `tel:${phone}`, textContent: phone })
) )
} }
if (props.mobile) { if (props.mobile) {
@ -301,7 +306,8 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
'div', 'div',
'', '',
container, container,
L.DomUtil.element('a', { L.DomUtil.element({
tagName: 'a',
href: `tel:${props.mobile}`, href: `tel:${props.mobile}`,
textContent: props.mobile, textContent: props.mobile,
}) })
@ -322,7 +328,8 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
'div', 'div',
'osm-link', 'osm-link',
container, container,
L.DomUtil.element('a', { L.DomUtil.element({
tagName: 'a',
href: `https://www.openstreetmap.org/${id}`, href: `https://www.openstreetmap.org/${id}`,
textContent: L._('See on OpenStreetMap'), textContent: L._('See on OpenStreetMap'),
}) })

View file

@ -143,7 +143,7 @@ U.Share = L.Class.extend({
} }
const iframeExporter = new U.IframeExporter(this.map) const iframeExporter = new U.IframeExporter(this.map)
const buildIframeCode = () => { const buildIframeCode = () => {
iframe.innerHTML = iframeExporter.build() iframe.textContent = iframeExporter.build()
exportUrl.value = window.location.protocol + iframeExporter.buildUrl() exportUrl.value = window.location.protocol + iframeExporter.buildUrl()
} }
buildIframeCode() buildIframeCode()

View file

@ -51,21 +51,22 @@ U.UI = L.Evented.extend({
close, close,
this this
) )
L.DomUtil.add('i', 'umap-close-icon', closeButton) L.DomUtil.create('i', 'umap-close-icon', closeButton)
const label = L.DomUtil.create('span', '', closeButton) const label = L.DomUtil.create('span', '', closeButton)
label.title = label.textContent = L._('Close') label.title = label.textContent = L._('Close')
L.DomUtil.add('div', '', this._alert, e.content) L.DomUtil.element({ tagName: 'div', innerHTML: e.content, parent: this._alert })
if (e.actions) { if (e.actions) {
let action, el, input let action, el, input
const form = L.DomUtil.add('div', 'umap-alert-actions', this._alert) const form = L.DomUtil.create('div', 'umap-alert-actions', this._alert)
for (let i = 0; i < e.actions.length; i++) { for (let i = 0; i < e.actions.length; i++) {
action = e.actions[i] action = e.actions[i]
if (action.input) { if (action.input) {
input = L.DomUtil.element( input = L.DomUtil.element({
'input', tagName: 'input',
{ className: 'umap-alert-input', placeholder: action.input }, parent: form,
form className: 'umap-alert-input',
) placeholder: action.input,
})
} }
el = L.DomUtil.createButton( el = L.DomUtil.createButton(
'umap-action', 'umap-action',
@ -97,7 +98,7 @@ U.UI = L.Evented.extend({
this.anchorTooltipAbsolute() this.anchorTooltipAbsolute()
} }
L.DomUtil.addClass(this.parent, 'umap-tooltip') L.DomUtil.addClass(this.parent, 'umap-tooltip')
this._tooltip.innerHTML = opts.content this._tooltip.innerHTML = U.Utils.escapeHTML(opts.content)
} }
this.TOOLTIP_ID = window.setTimeout(L.bind(showIt, this), opts.delay || 0) this.TOOLTIP_ID = window.setTimeout(L.bind(showIt, this), opts.delay || 0)
const id = this.TOOLTIP_ID const id = this.TOOLTIP_ID

View file

@ -45,3 +45,24 @@ def test_create_map_with_cursor(page, live_server, tilelayer):
"z-index: 200;" "z-index: 200;"
), ),
) )
def test_cannot_put_script_tag_in_datalayer_name_or_description(
openmap, live_server, page, tilelayer
):
page.goto(f"{live_server.url}{openmap.get_absolute_url()}")
page.get_by_role("button", name="Edit").click()
page.get_by_role("link", name="Manage layers").click()
page.get_by_role("button", name="Add a layer").click()
page.locator('input[name="name"]').click()
page.locator('input[name="name"]').fill('<script>alert("attack")</script>')
page.locator(".umap-field-description textarea").click()
page.locator(".umap-field-description textarea").fill(
'<p>before <script>alert("attack")</script> after</p>'
)
page.get_by_role("button", name="Save").click()
page.get_by_role("button", name="About").click()
# Title should contain raw HTML (we are using textContent)
expect(page.get_by_text('<script>alert("attack")</script>')).to_be_visible()
# Description should contain escaped HTML
expect(page.get_by_text("before after")).to_be_visible()

View file

@ -112,3 +112,13 @@ def test_map_should_display_custom_tilelayer(map, live_server, tilelayers, page)
iconTiles = page.locator(".leaflet-iconLayers .leaflet-iconLayers-layer") iconTiles = page.locator(".leaflet-iconLayers .leaflet-iconLayers-layer")
# The second of the list should be the current # The second of the list should be the current
expect(iconTiles.nth(1)).to_have_css("background-image", url_pattern) expect(iconTiles.nth(1)).to_have_css("background-image", url_pattern)
def test_can_have_smart_text_in_attribution(tilelayer, map, live_server, page):
map.settings["properties"]["tilelayer"]["attribution"] = (
"&copy; [[http://www.openstreetmap.org/copyright|OpenStreetMap]] contributors"
)
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.get_by_text("© OpenStreetMap contributors")).to_be_visible()
expect(page.get_by_role("link", name="OpenStreetMap")).to_be_visible()