From bfd7613d68b2056d3f7e0f48e8503977b3c3203e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Thu, 9 May 2024 16:10:38 +0200 Subject: [PATCH] chore(sync): Ensure properties can be updated before doing it. When receiving a message, this checks the given properties belong to the "subject" before applying the message. --- umap/static/umap/js/modules/schema.js | 43 ++++--- umap/static/umap/js/modules/sync/updaters.js | 15 ++- umap/static/umap/js/modules/sync/websocket.js | 1 - umap/static/umap/js/modules/utils.js | 28 ++++- umap/static/umap/unittests/utils.js | 117 +++++++++++++++--- 5 files changed, 163 insertions(+), 41 deletions(-) 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 2f0cb57a..dca5fdfc 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 c405b4d4..264b0d85 100644 --- a/umap/static/umap/unittests/utils.js +++ b/umap/static/umap/unittests/utils.js @@ -461,13 +461,12 @@ describe('Utils', function () { }) describe('#normalize()', function () { - it('should remove accents', - function () { - // French é - assert.equal(Utils.normalize('aéroport'), 'aeroport') - // American é - assert.equal(Utils.normalize('aéroport'), 'aeroport') - }) + it('should remove accents', function () { + // French é + assert.equal(Utils.normalize('aéroport'), 'aeroport') + // American é + assert.equal(Utils.normalize('aéroport'), 'aeroport') + }) }) describe('#sortFeatures()', function () { @@ -530,17 +529,17 @@ describe('Utils', function () { }) }) - describe("#copyJSON", function () { + describe('#copyJSON', function () { it('should actually copy the JSON', function () { - let originalJSON = { "some": "json" } + let originalJSON = { some: 'json' } let returned = Utils.CopyJSON(originalJSON) // Change the original JSON - originalJSON["anotherKey"] = "value" + originalJSON['anotherKey'] = 'value' // ensure the two aren't the same object assert.notEqual(returned, originalJSON) - assert.deepEqual(returned, { "some": "json" }) + assert.deepEqual(returned, { some: 'json' }) }) }) @@ -597,21 +596,105 @@ 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(), "2024-03-04T00:00:00.000Z") + assert.equal( + Utils.parseNaiveDate('2024/03/04').toISOString(), + '2024-03-04T00:00:00.000Z' + ) }) it('should parse a datetime', () => { - assert.equal(Utils.parseNaiveDate("2024/03/04 12:13:14").toISOString(), "2024-03-04T00:00:00.000Z") + assert.equal( + Utils.parseNaiveDate('2024/03/04 12:13:14').toISOString(), + '2024-03-04T00:00:00.000Z' + ) }) it('should parse an iso datetime', () => { - assert.equal(Utils.parseNaiveDate("2024-03-04T00:00:00.000Z").toISOString(), "2024-03-04T00:00:00.000Z") + assert.equal( + Utils.parseNaiveDate('2024-03-04T00:00:00.000Z').toISOString(), + '2024-03-04T00:00:00.000Z' + ) }) it('should parse a GMT time', () => { - assert.equal(Utils.parseNaiveDate("04 Mar 2024 00:12:00 GMT").toISOString(), "2024-03-04T00:00:00.000Z") + assert.equal( + Utils.parseNaiveDate('04 Mar 2024 00:12:00 GMT').toISOString(), + '2024-03-04T00:00:00.000Z' + ) }) it('should parse a GMT time with explicit timezone', () => { - assert.equal(Utils.parseNaiveDate("Thu, 04 Mar 2024 00:00:00 GMT+0300").toISOString(), "2024-03-03T00:00:00.000Z") + assert.equal( + Utils.parseNaiveDate('Thu, 04 Mar 2024 00:00:00 GMT+0300').toISOString(), + '2024-03-03T00:00:00.000Z' + ) }) }) })