diff --git a/CHANGELOG.md b/CHANGELOG.md index 11cee770713..b45529e24b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## Version 25.03.xx Fixes: - [events] Fix sum chart tooltip displaying the raw floating-point value instead of a rounded number +- [events] Fixed event descriptions (and custom names / count-sum-dur labels) not showing on the Events page for events whose key contains special characters (`.`, `$`, `\`, `&`, `<`, `>`, `"`, `'`) + +Enterprise Fixes: +- [data-manager] Fixed editing an event whose key contains `&` creating undeletable duplicate rows in the events table Security Fixes: - [compliance-hub] The consents table now returns a fixed set of fields; a projection supplied on the request is no longer used to widen the response beyond the consent columns diff --git a/frontend/express/public/core/events/javascripts/countly.details.models.js b/frontend/express/public/core/events/javascripts/countly.details.models.js index 6122e069580..7c9e83b9a20 100644 --- a/frontend/express/public/core/events/javascripts/countly.details.models.js +++ b/frontend/express/public/core/events/javascripts/countly.details.models.js @@ -231,7 +231,8 @@ return str.replace(/&/g, "&").replace(//g, ">").replace(/<=/g, "≤").replace(/>=/g, "≥"); }, getEventLongName: function(eventKey, eventMap) { - var mapKey = eventKey.replace(/\\/g, "\\\\").replace(/\$/g, "\\u0024").replace(/\./g, "\\u002e"); + //events.map is keyed by the raw event key, exactly as it appears in events.list, so do not escape it here + var mapKey = eventKey; if (eventMap && eventMap[mapKey] && eventMap[mapKey].name) { return eventMap[mapKey].name; } @@ -244,7 +245,8 @@ var allEvents = context.state.allEventsData; var groupData = context.state.groupData.displayMap; var eventMap = allEvents.map; - var mapKey = context.state.selectedEventName.replace(/\\/g, "\\\\").replace(/\$/g, "\\u0024").replace(/\./g, '\\u002e'); + //events.map is keyed by the raw event key, exactly as it appears in events.list, so do not escape it here + var mapKey = context.state.selectedEventName; var countString = (mapKey.startsWith('[CLY]_group') && groupData.c) ? groupData.c : (eventMap && eventMap[mapKey] && eventMap[mapKey].count) ? eventMap[mapKey].count : jQuery.i18n.map["events.table.count"]; var sumString = (mapKey.startsWith('[CLY]_group') && groupData.s) ? groupData.s : (eventMap && eventMap[mapKey] && eventMap[mapKey].sum) ? eventMap[mapKey].sum : jQuery.i18n.map["events.table.sum"]; var durString = (mapKey.startsWith('[CLY]_group') && groupData.d) ? groupData.d : (eventMap && eventMap[mapKey] && eventMap[mapKey].dur) ? eventMap[mapKey].dur : jQuery.i18n.map["events.table.dur"]; @@ -893,9 +895,7 @@ return countlyAllEvents.service.fetchAllEventsData(context, period) .then(function(res) { if (res) { - if (Array.isArray(res.list)) { - res.list = res.list.map(eventName => countlyCommon.unescapeHtml(eventName)); - } + // setAllEventsData decodes res.list and res.map keys in place context.commit("setAllEventsData", res); var is_group = false; if (context.state.selectedEventName && context.state.selectedEventName.startsWith('[CLY]_group')) { @@ -1106,6 +1106,29 @@ var allEventsMutations = { setAllEventsData: function(state, value) { + // The API HTML-escapes every key and value on the way out (common.returnOutput), + // but the rest of this store works with raw event keys - selectedEventName is + // taken from `list`. Normalise `list` and the `map` keys together here, the one + // point both fetch paths commit through, so lookups like map[selectedEventName] + // don't silently miss for keys containing & < > " '. Keys without those chars + // (dots, dashes) were unaffected, which is why only some events lost their + // description on the Events page. + if (value && Array.isArray(value.list)) { + value.list = value.list.map(function(eventName) { + return countlyCommon.unescapeHtml(eventName); + }); + } + if (value && value.map && typeof value.map === "object") { + //null prototype: event keys are arbitrary strings, and assigning a + //"__proto__" key on a plain object reparents the map instead of adding + //an own property, which would let unrelated key lookups resolve through + //the assigned value + var decodedMap = Object.create(null); + Object.keys(value.map).forEach(function(eventKey) { + decodedMap[countlyCommon.unescapeHtml(eventKey)] = value.map[eventKey]; + }); + value.map = decodedMap; + } state.allEventsData = value; }, setAllEventsList: function(state, value) { diff --git a/plugins/data-manager/frontend/public/javascripts/countly.views.js b/plugins/data-manager/frontend/public/javascripts/countly.views.js index c752e7f9e3b..e08ef2ee13a 100644 --- a/plugins/data-manager/frontend/public/javascripts/countly.views.js +++ b/plugins/data-manager/frontend/public/javascripts/countly.views.js @@ -700,7 +700,9 @@ var isVisible = command === 'visible'; var events = []; rows.forEach(function(row) { - events.push(row.key); + // row.key is still API-escaped; change_visibility writes events.map[key] + // directly, so submitting it raw would create a ghost map entry + events.push(countlyCommon.unescapeHtml(row.key)); }); this.$store.dispatch('countlyDataManager/changeVisibility', { events: events, isVisible: isVisible }).then(function() { countlyEvent.refreshEvents(); @@ -1338,8 +1340,22 @@ }, handleEdit: function() { var event = JSON.parse(JSON.stringify(this.event)); - event.segments = this.segments; + event.segments = JSON.parse(JSON.stringify(this.segments)); event.isEditMode = true; + // The store holds values exactly as the API returned them, and common.returnOutput + // HTML-escapes every string on the way out. The events table decodes these before + // opening the same drawer (see the 'dm-open-edit-event-drawer' handler) - do the + // same here, otherwise a key like "a & b" is submitted back as "a & b" and + // forks a second drill_meta doc / events.map entry that shows up as a ghost row. + event.key = countlyCommon.unescapeHtml(event.key); + event.e = countlyCommon.unescapeHtml(event.e); + event.name = countlyCommon.unescapeHtml(event.name); + event.description = countlyCommon.unescapeHtml(event.description); + event.categoryName = countlyCommon.unescapeHtml(event.categoryName); + event.segments = event.segments.map(function(seg) { + seg.name = countlyCommon.unescapeHtml(seg.name); + return seg; + }); this.openDrawer("events", event); }, handleEditSegment: function(seg) {