diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f6254943cd..8f2700172ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Version 24.05.52 +Fixes: +- [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 + ## Version 24.05.51 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 4b22ff19ae2..1d67ad7aae8 100644 --- a/frontend/express/public/core/events/javascripts/countly.details.models.js +++ b/frontend/express/public/core/events/javascripts/countly.details.models.js @@ -236,7 +236,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; } @@ -249,7 +250,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"]; @@ -886,9 +888,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); if ((!context.state.selectedEventName) || (res.map && res.map[context.state.selectedEventName] && !res.map[context.state.selectedEventName].is_visible) || (res.list && res.list.indexOf(context.state.selectedEventName) === -1)) { var appId = countlyCommon.ACTIVE_APP_ID; @@ -1090,6 +1090,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 ef5f22390e2..5ee9a5839db 100644 --- a/plugins/data-manager/frontend/public/javascripts/countly.views.js +++ b/plugins/data-manager/frontend/public/javascripts/countly.views.js @@ -696,7 +696,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(); @@ -1324,8 +1326,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) {