diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js index 9602628f..74856308 100644 --- a/umap/static/umap/js/modules/schema.js +++ b/umap/static/umap/js/modules/schema.js @@ -99,26 +99,26 @@ export const SCHEMA = { description: { type: 'Text', impacts: ['ui'], - belongsTo: ['map', 'datalayer'], + belongsTo: ['map', 'datalayer', 'feature'], label: translate('description'), helpEntries: 'textFormatting', }, displayOnLoad: { type: Boolean, impacts: [], - belongsTo: ['datalayer'], // XXX ? + belongsTo: ['datalayer'], }, displayPopupFooter: { type: Boolean, impacts: ['ui'], - belongsTo: ['map'], // XXX ? + belongsTo: ['map'], label: translate('Do you want to display popup footer?'), default: false, }, easing: { type: Boolean, impacts: [], - belongsTo: ['feature'], // XXX ? + belongsTo: ['map', 'datalayer', 'feature'], default: false, }, editinosmControl: { @@ -140,7 +140,7 @@ export const SCHEMA = { facetKey: { type: String, impacts: ['ui'], - belongsTo: ['datalayer'], + belongsTo: ['map', 'datalayer'], }, fill: { type: Boolean, @@ -174,12 +174,12 @@ export const SCHEMA = { filterKey: { type: String, impacts: [], - belongsTo: ['map', 'datalayer', 'feature'], // XXX ? + belongsTo: ['map', 'datalayer', 'feature'], }, fromZoom: { type: Number, impacts: [], // not needed - belongsTo: ['map'], // XXX ? + belongsTo: ['map', 'datalayer'], label: translate('From zoom'), helpText: translate('Optional.'), }, @@ -232,7 +232,7 @@ export const SCHEMA = { inCaption: { type: Boolean, impacts: ['ui'], - belongsTo: [], // XXX ? + belongsTo: ['datalayer'], }, interactive: { type: Boolean, @@ -268,7 +268,7 @@ export const SCHEMA = { labelKey: { type: String, impacts: ['data'], - belongsTo: ['map', 'datalayer', 'feature'], // XXX ? + belongsTo: ['map', 'datalayer', 'feature'], helpEntries: 'labelKey', placeholder: translate('Default: name'), label: translate('Label key'), @@ -277,7 +277,7 @@ export const SCHEMA = { licence: { type: String, impacts: ['ui'], - belongsTo: ['map', 'datalayer'], // XXX ? + belongsTo: ['map', 'datalayer'], label: translate('licence'), }, limitBounds: { @@ -354,7 +354,7 @@ export const SCHEMA = { outlink: { type: String, impacts: ['data'], - belongsTo: ['feature'], + belongsTo: ['map', 'datalayer', 'feature'], label: translate('Link to…'), helpEntries: 'outlink', placeholder: 'http://...', @@ -363,7 +363,7 @@ export const SCHEMA = { outlinkTarget: { type: String, impacts: ['data'], - belongsTo: ['feature'], + belongsTo: ['map', 'datalayer', 'feature'], label: translate('Open link in…'), inheritable: true, default: 'blank', @@ -405,7 +405,7 @@ export const SCHEMA = { popupShape: { type: String, impacts: [], // not needed - belongsTo: ['map', 'datalayer', 'feature'], // XXX ? + belongsTo: ['map', 'datalayer', 'feature'], label: translate('Popup shape'), inheritable: true, choices: [ @@ -418,7 +418,7 @@ export const SCHEMA = { popupTemplate: { type: String, impacts: [], // not needed - belongsTo: ['map', 'datalayer', 'feature'], // XXX ? + belongsTo: ['map', 'datalayer', 'feature'], label: translate('Popup content style'), inheritable: true, choices: [ @@ -480,7 +480,7 @@ export const SCHEMA = { slugKey: { type: String, impacts: [], - belongsTo: ['map', 'datalayer', 'feature'], // XXX ? + belongsTo: ['map', 'datalayer', 'feature'], }, smoothFactor: { type: Number, @@ -538,19 +538,19 @@ export const SCHEMA = { toZoom: { type: Number, impacts: [], // not needed - belongsTo: ['map'], + belongsTo: ['map', 'datalayer'], label: translate('To zoom'), helpText: translate('Optional.'), }, type: { type: 'String', impacts: ['data'], - belongsTo: [], // XXX ? + belongsTo: ['datalayer'], }, weight: { type: Number, impacts: ['data'], - belongsTo: ['feature'], // XXX ?, + belongsTo: ['map', 'datalayer', 'feature'], min: 1, max: 20, step: 1, @@ -574,10 +574,15 @@ export const SCHEMA = { zoomTo: { type: Number, impacts: [], // not need to update the view - belongsTo: ['map'], + belongsTo: ['map', 'datalayer', 'feature'], placeholder: translate('Inherit'), helpEntries: 'zoomTo', label: translate('Default zoom level'), inheritable: true, }, + _latlng: { + type: Object, + impacts: ['data'], + belongsTo: ['feature'], + }, } diff --git a/umap/static/umap/js/modules/sync/updaters.js b/umap/static/umap/js/modules/sync/updaters.js index c9e7e627..080cdf72 100644 --- a/umap/static/umap/js/modules/sync/updaters.js +++ b/umap/static/umap/js/modules/sync/updaters.js @@ -1,3 +1,5 @@ +import { propertyBelongsTo } from '../utils.js' + /** * This file contains the updaters: classes that are able to convert messages * received from another party (or the server) to changes on the map. @@ -31,9 +33,16 @@ class BaseUpdater { return this.map.defaultEditDataLayer() } - applyMessage(message) { - let { verb } = message - return this[verb](message) + applyMessage(payload) { + let { verb, subject } = payload + + if (verb == 'update') { + if (!propertyBelongsTo(payload.key, subject)) { + console.error('Invalid message received', payload) + return // Do not apply the message + } + } + return this[verb](payload) } } diff --git a/umap/static/umap/js/modules/sync/websocket.js b/umap/static/umap/js/modules/sync/websocket.js index 87540168..e575bed6 100644 --- a/umap/static/umap/js/modules/sync/websocket.js +++ b/umap/static/umap/js/modules/sync/websocket.js @@ -9,7 +9,6 @@ export class WebSocketTransport { } onMessage(wsMessage) { - // XXX validate incoming data. this.receiver.dispatch(JSON.parse(wsMessage.data)) } diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js index eca2c44e..7f5a213b 100644 --- a/umap/static/umap/js/modules/utils.js +++ b/umap/static/umap/js/modules/utils.js @@ -30,7 +30,8 @@ export function checkId(string) { * * Return an array of unique impacts. * - * @param {fields} list[fields] + * @param {fields} list[fields] + * @param object schema object. If ommited, global U.SCHEMA will be used. * @returns Array[string] */ export function getImpactsFromSchema(fields, schema) { @@ -53,6 +54,31 @@ export function getImpactsFromSchema(fields, schema) { return Array.from(impacted) } +/** + * Checks the given property belongs to the given subject, according to the schema. + * + * @param srtring property + * @param string subject + * @param object schema object. If ommited, global U.SCHEMA will be used. + * @returns Bool + */ +export function propertyBelongsTo(property, subject, schema) { + schema = schema || U.SCHEMA + if (subject === 'feature') { + property = property.replace('properties.', '').replace('_umap_options.', '') + } + property = property.replace('options.', '') + console.log(property) + const splits = property.split('.') + const nested = splits.length > 1 + if (nested) property = splits[0] + if (!Object.keys(schema).includes(property)) return false + if (nested) { + if (schema[property].type !== Object) return false + } + return schema[property].belongsTo.includes(subject) +} + /** * Import DOM purify, and initialize it. * diff --git a/umap/static/umap/unittests/utils.js b/umap/static/umap/unittests/utils.js index 99b9c8a4..cb6e8d23 100644 --- a/umap/static/umap/unittests/utils.js +++ b/umap/static/umap/unittests/utils.js @@ -623,7 +623,76 @@ describe('Utils', function () { assert.deepEqual(getImpactsFromSchema(['foo', 'bar', 'baz'], schema), ['A', 'B']) }) }) - describe('parseNaiveDate', () => { + + describe('#propertyBelongsTo', () => { + it('should return false on unexisting property', function () { + let schema = {} + assert.deepEqual(Utils.propertyBelongsTo('foo', 'map', schema), false) + }) + it('should return false if subject is not listed', function () { + let schema = { + foo: { belongsTo: ['feature'] }, + } + assert.deepEqual(Utils.propertyBelongsTo('foo', 'map', schema), false) + }) + it('should return true if subject is listed', function () { + let schema = { + foo: { belongsTo: ['map'] }, + } + assert.deepEqual(Utils.propertyBelongsTo('foo', 'map', schema), true) + }) + it('should remove the `options.` prefix before checking', function () { + let schema = { + foo: { belongsTo: ['map'] }, + } + assert.deepEqual(Utils.propertyBelongsTo('options.foo', 'map', schema), true) + }) + + it('Accepts setting properties on objects', function () { + let schema = { + foo: { + type: Object, + belongsTo: ['map'], + }, + } + assert.deepEqual(Utils.propertyBelongsTo('options.foo.name', 'map', schema), true) + }) + + it('Rejects setting properties on non-objects', function () { + let schema = { + foo: { + type: String, + belongsTo: ['map'], + }, + } + assert.deepEqual( + Utils.propertyBelongsTo('options.foo.name', 'map', schema), + false + ) + }) + + it('when subject = feature, should filter the `properties.`', function () { + let schema = { + foo: { belongsTo: ['feature'] }, + } + assert.deepEqual( + Utils.propertyBelongsTo('properties.foo', 'feature', schema), + true + ) + }) + + it('On features, should filter the `_umap_options.`', function () { + let schema = { + foo: { belongsTo: ['feature'] }, + } + assert.deepEqual( + Utils.propertyBelongsTo('properties._umap_options.foo', 'feature', schema), + true + ) + }) + }) + + describe('#parseNaiveDate', () => { it('should parse a date', () => { assert.equal( Utils.parseNaiveDate('2024/03/04').toISOString(),