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.
This commit is contained in:
Alexis Métaireau 2024-05-09 16:10:38 +02:00
parent 25ccdde0b0
commit 2fafad714e
5 changed files with 133 additions and 25 deletions

View file

@ -99,26 +99,26 @@ export const SCHEMA = {
description: { description: {
type: 'Text', type: 'Text',
impacts: ['ui'], impacts: ['ui'],
belongsTo: ['map', 'datalayer'], belongsTo: ['map', 'datalayer', 'feature'],
label: translate('description'), label: translate('description'),
helpEntries: 'textFormatting', helpEntries: 'textFormatting',
}, },
displayOnLoad: { displayOnLoad: {
type: Boolean, type: Boolean,
impacts: [], impacts: [],
belongsTo: ['datalayer'], // XXX ? belongsTo: ['datalayer'],
}, },
displayPopupFooter: { displayPopupFooter: {
type: Boolean, type: Boolean,
impacts: ['ui'], impacts: ['ui'],
belongsTo: ['map'], // XXX ? belongsTo: ['map'],
label: translate('Do you want to display popup footer?'), label: translate('Do you want to display popup footer?'),
default: false, default: false,
}, },
easing: { easing: {
type: Boolean, type: Boolean,
impacts: [], impacts: [],
belongsTo: ['feature'], // XXX ? belongsTo: ['map', 'datalayer', 'feature'],
default: false, default: false,
}, },
editinosmControl: { editinosmControl: {
@ -140,7 +140,7 @@ export const SCHEMA = {
facetKey: { facetKey: {
type: String, type: String,
impacts: ['ui'], impacts: ['ui'],
belongsTo: ['datalayer'], belongsTo: ['map', 'datalayer'],
}, },
fill: { fill: {
type: Boolean, type: Boolean,
@ -174,12 +174,12 @@ export const SCHEMA = {
filterKey: { filterKey: {
type: String, type: String,
impacts: [], impacts: [],
belongsTo: ['map', 'datalayer', 'feature'], // XXX ? belongsTo: ['map', 'datalayer', 'feature'],
}, },
fromZoom: { fromZoom: {
type: Number, type: Number,
impacts: [], // not needed impacts: [], // not needed
belongsTo: ['map'], // XXX ? belongsTo: ['map', 'datalayer'],
label: translate('From zoom'), label: translate('From zoom'),
helpText: translate('Optional.'), helpText: translate('Optional.'),
}, },
@ -232,7 +232,7 @@ export const SCHEMA = {
inCaption: { inCaption: {
type: Boolean, type: Boolean,
impacts: ['ui'], impacts: ['ui'],
belongsTo: [], // XXX ? belongsTo: ['datalayer'],
}, },
interactive: { interactive: {
type: Boolean, type: Boolean,
@ -268,7 +268,7 @@ export const SCHEMA = {
labelKey: { labelKey: {
type: String, type: String,
impacts: ['data'], impacts: ['data'],
belongsTo: ['map', 'datalayer', 'feature'], // XXX ? belongsTo: ['map', 'datalayer', 'feature'],
helpEntries: 'labelKey', helpEntries: 'labelKey',
placeholder: translate('Default: name'), placeholder: translate('Default: name'),
label: translate('Label key'), label: translate('Label key'),
@ -277,7 +277,7 @@ export const SCHEMA = {
licence: { licence: {
type: String, type: String,
impacts: ['ui'], impacts: ['ui'],
belongsTo: ['map', 'datalayer'], // XXX ? belongsTo: ['map', 'datalayer'],
label: translate('licence'), label: translate('licence'),
}, },
limitBounds: { limitBounds: {
@ -354,7 +354,7 @@ export const SCHEMA = {
outlink: { outlink: {
type: String, type: String,
impacts: ['data'], impacts: ['data'],
belongsTo: ['feature'], belongsTo: ['map', 'datalayer', 'feature'],
label: translate('Link to…'), label: translate('Link to…'),
helpEntries: 'outlink', helpEntries: 'outlink',
placeholder: 'http://...', placeholder: 'http://...',
@ -363,7 +363,7 @@ export const SCHEMA = {
outlinkTarget: { outlinkTarget: {
type: String, type: String,
impacts: ['data'], impacts: ['data'],
belongsTo: ['feature'], belongsTo: ['map', 'datalayer', 'feature'],
label: translate('Open link in…'), label: translate('Open link in…'),
inheritable: true, inheritable: true,
default: 'blank', default: 'blank',
@ -405,7 +405,7 @@ export const SCHEMA = {
popupShape: { popupShape: {
type: String, type: String,
impacts: [], // not needed impacts: [], // not needed
belongsTo: ['map', 'datalayer', 'feature'], // XXX ? belongsTo: ['map', 'datalayer', 'feature'],
label: translate('Popup shape'), label: translate('Popup shape'),
inheritable: true, inheritable: true,
choices: [ choices: [
@ -418,7 +418,7 @@ export const SCHEMA = {
popupTemplate: { popupTemplate: {
type: String, type: String,
impacts: [], // not needed impacts: [], // not needed
belongsTo: ['map', 'datalayer', 'feature'], // XXX ? belongsTo: ['map', 'datalayer', 'feature'],
label: translate('Popup content style'), label: translate('Popup content style'),
inheritable: true, inheritable: true,
choices: [ choices: [
@ -480,7 +480,7 @@ export const SCHEMA = {
slugKey: { slugKey: {
type: String, type: String,
impacts: [], impacts: [],
belongsTo: ['map', 'datalayer', 'feature'], // XXX ? belongsTo: ['map', 'datalayer', 'feature'],
}, },
smoothFactor: { smoothFactor: {
type: Number, type: Number,
@ -538,19 +538,19 @@ export const SCHEMA = {
toZoom: { toZoom: {
type: Number, type: Number,
impacts: [], // not needed impacts: [], // not needed
belongsTo: ['map'], belongsTo: ['map', 'datalayer'],
label: translate('To zoom'), label: translate('To zoom'),
helpText: translate('Optional.'), helpText: translate('Optional.'),
}, },
type: { type: {
type: 'String', type: 'String',
impacts: ['data'], impacts: ['data'],
belongsTo: [], // XXX ? belongsTo: ['datalayer'],
}, },
weight: { weight: {
type: Number, type: Number,
impacts: ['data'], impacts: ['data'],
belongsTo: ['feature'], // XXX ?, belongsTo: ['map', 'datalayer', 'feature'],
min: 1, min: 1,
max: 20, max: 20,
step: 1, step: 1,
@ -574,10 +574,15 @@ export const SCHEMA = {
zoomTo: { zoomTo: {
type: Number, type: Number,
impacts: [], // not need to update the view impacts: [], // not need to update the view
belongsTo: ['map'], belongsTo: ['map', 'datalayer', 'feature'],
placeholder: translate('Inherit'), placeholder: translate('Inherit'),
helpEntries: 'zoomTo', helpEntries: 'zoomTo',
label: translate('Default zoom level'), label: translate('Default zoom level'),
inheritable: true, inheritable: true,
}, },
_latlng: {
type: Object,
impacts: ['data'],
belongsTo: ['feature'],
},
} }

View file

@ -1,3 +1,5 @@
import { propertyBelongsTo } from '../utils.js'
/** /**
* This file contains the updaters: classes that are able to convert messages * This file contains the updaters: classes that are able to convert messages
* received from another party (or the server) to changes on the map. * received from another party (or the server) to changes on the map.
@ -31,9 +33,16 @@ class BaseUpdater {
return this.map.defaultEditDataLayer() return this.map.defaultEditDataLayer()
} }
applyMessage(message) { applyMessage(payload) {
let { verb } = message let { verb, subject } = payload
return this[verb](message)
if (verb == 'update') {
if (!propertyBelongsTo(payload.key, subject)) {
console.error('Invalid message received', payload)
return // Do not apply the message
}
}
return this[verb](payload)
} }
} }

View file

@ -9,7 +9,6 @@ export class WebSocketTransport {
} }
onMessage(wsMessage) { onMessage(wsMessage) {
// XXX validate incoming data.
this.receiver.dispatch(JSON.parse(wsMessage.data)) this.receiver.dispatch(JSON.parse(wsMessage.data))
} }

View file

@ -30,7 +30,8 @@ export function checkId(string) {
* *
* Return an array of unique impacts. * 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] * @returns Array[string]
*/ */
export function getImpactsFromSchema(fields, schema) { export function getImpactsFromSchema(fields, schema) {
@ -53,6 +54,31 @@ export function getImpactsFromSchema(fields, schema) {
return Array.from(impacted) 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. * Import DOM purify, and initialize it.
* *

View file

@ -623,7 +623,76 @@ describe('Utils', function () {
assert.deepEqual(getImpactsFromSchema(['foo', 'bar', 'baz'], schema), ['A', 'B']) 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', () => { it('should parse a date', () => {
assert.equal( assert.equal(
Utils.parseNaiveDate('2024/03/04').toISOString(), Utils.parseNaiveDate('2024/03/04').toISOString(),