From c0da919697bcf06d76ebb539e951bc8c4f46fa9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Tue, 24 Oct 2023 14:28:57 +0200 Subject: [PATCH] [REF] mail: discuss model html field and trusted insert This commit allows to define html fields in discuss models, so that when insert is trusted they are automatically markup. By default, all model insert are untrusted. To make a trusted insertion, we should pass 2nd parameter `{ html: true }` to an `insert()` method. The param is intentionally named `html` instead of `trusted`, so that it triggers ci/security for reviewers awareness. It's still possible to immediately assign a markup on the html fields, like before. This changes simplifies the code, so that we don't have to dissect model data everytime and make sure all bits are properly markup. The dissect operation added many LOCs and we frequently missed to markup the data in some flows. Being able to flag html fields at model definition and flagging an insert safe keeps the security concern while helping properly markup-ing all the data. Part-of: odoo/odoo#139501 --- .../src/core/common/attachment_model.js | 2 +- .../src/core/common/canned_response_model.js | 2 +- .../src/core/common/channel_member_model.js | 2 +- .../src/core/common/chat_window_model.js | 6 +- .../static/src/core/common/composer_model.js | 2 +- .../core/common/discuss_app_category_model.js | 2 +- .../src/core/common/discuss_app_model.js | 2 +- .../static/src/core/common/failure_model.js | 5 +- .../static/src/core/common/follower_model.js | 2 +- .../src/core/common/link_preview_model.js | 2 +- .../core/common/mail_core_common_service.js | 25 ++++--- .../static/src/core/common/message_model.js | 7 +- .../core/common/message_reactions_model.js | 2 +- .../static/src/core/common/message_service.js | 29 +------- .../src/core/common/notification_model.js | 2 +- .../static/src/core/common/persona_model.js | 2 +- addons/mail/static/src/core/common/record.js | 35 ++++++++-- .../static/src/core/common/store_service.js | 25 ++++++- .../static/src/core/common/thread_model.js | 2 +- .../static/src/core/common/thread_service.js | 70 +++++-------------- .../static/src/core/web/activity_model.js | 12 +++- .../src/core/web/mail_core_web_service.js | 23 +++--- .../src/core/web/thread_service_patch.js | 7 +- .../discuss/call/common/rtc_session_model.js | 5 +- .../common/discuss_core_common_service.js | 29 ++++---- .../message_pin/common/message_pin_service.js | 12 ++-- addons/mail/static/tests/core/record_tests.js | 14 ++++ .../tests/messaging_menu_patch_tests.js | 2 +- 28 files changed, 170 insertions(+), 160 deletions(-) diff --git a/addons/mail/static/src/core/common/attachment_model.js b/addons/mail/static/src/core/common/attachment_model.js index 6869a2fa821..3e6d58aa187 100644 --- a/addons/mail/static/src/core/common/attachment_model.js +++ b/addons/mail/static/src/core/common/attachment_model.js @@ -19,7 +19,7 @@ export class Attachment extends FileModelMixin(Record) { if (!("id" in data)) { throw new Error("Cannot insert attachment: id is missing in data"); } - return super.insert(data); + return super.insert(...arguments); } static new(data) { /** @type {import("models").Attachment} */ diff --git a/addons/mail/static/src/core/common/canned_response_model.js b/addons/mail/static/src/core/common/canned_response_model.js index d95f333d153..91729273d31 100644 --- a/addons/mail/static/src/core/common/canned_response_model.js +++ b/addons/mail/static/src/core/common/canned_response_model.js @@ -12,7 +12,7 @@ export class CannedResponse extends Record { } /** @returns {import("models").CannedResponse} */ static insert(data) { - return super.insert(data); + return super.insert(...arguments); } /** @type {number} */ diff --git a/addons/mail/static/src/core/common/channel_member_model.js b/addons/mail/static/src/core/common/channel_member_model.js index cccf63fffaf..1d5f83f8e65 100644 --- a/addons/mail/static/src/core/common/channel_member_model.js +++ b/addons/mail/static/src/core/common/channel_member_model.js @@ -19,7 +19,7 @@ export class ChannelMember extends Record { } /** @returns {import("models").ChannelMember} */ static insert(data) { - return super.insert(data); + return super.insert(...arguments); } /** @type {number} */ diff --git a/addons/mail/static/src/core/common/chat_window_model.js b/addons/mail/static/src/core/common/chat_window_model.js index 90915d39e58..1de41008359 100644 --- a/addons/mail/static/src/core/common/chat_window_model.js +++ b/addons/mail/static/src/core/common/chat_window_model.js @@ -15,11 +15,15 @@ export class ChatWindow extends Record { static get(data) { return super.get(data); } + /** @returns {import("models").ChatWindow} */ + static insert() { + return super.insert(...arguments); + } /** * @param {ChatWindowData} [data] * @returns {import("models").ChatWindow} */ - static insert(data = {}) { + static _insert(data = {}) { const chatWindow = this.store.discuss.chatWindows.find((c) => c.thread?.eq(data.thread)); if (!chatWindow) { /** @type {import("models").ChatWindow} */ diff --git a/addons/mail/static/src/core/common/composer_model.js b/addons/mail/static/src/core/common/composer_model.js index 5ce1d07524f..1a23f52ff13 100644 --- a/addons/mail/static/src/core/common/composer_model.js +++ b/addons/mail/static/src/core/common/composer_model.js @@ -14,7 +14,7 @@ export class Composer extends Record { if (Boolean(message) === Boolean(thread)) { throw new Error("Composer shall have a thread xor a message."); } - return super.insert(data); + return super.insert(...arguments); } attachments = Record.many("Attachment"); diff --git a/addons/mail/static/src/core/common/discuss_app_category_model.js b/addons/mail/static/src/core/common/discuss_app_category_model.js index 8bc371d38c4..6079ea0bcc0 100644 --- a/addons/mail/static/src/core/common/discuss_app_category_model.js +++ b/addons/mail/static/src/core/common/discuss_app_category_model.js @@ -10,7 +10,7 @@ export class DiscussAppCategory extends Record { } /** @returns {import("models").DiscussAppCategory} */ static insert(data) { - return super.insert(data); + return super.insert(...arguments); } /** @type {string} */ diff --git a/addons/mail/static/src/core/common/discuss_app_model.js b/addons/mail/static/src/core/common/discuss_app_model.js index dcdc85de717..c35eeacf351 100644 --- a/addons/mail/static/src/core/common/discuss_app_model.js +++ b/addons/mail/static/src/core/common/discuss_app_model.js @@ -39,7 +39,7 @@ export class DiscussApp extends Record { } /** @returns {import("models").DiscussApp} */ static insert(data) { - return super.insert(data); + return super.insert(...arguments); } /** @type {'mailbox'|'all'|'channel'|'chat'|'livechat'} */ diff --git a/addons/mail/static/src/core/common/failure_model.js b/addons/mail/static/src/core/common/failure_model.js index b1a2e3feac0..3baed0fdeb2 100644 --- a/addons/mail/static/src/core/common/failure_model.js +++ b/addons/mail/static/src/core/common/failure_model.js @@ -27,8 +27,11 @@ export class Failure extends Record { } /** @returns {import("models").Failure} */ static insert(data) { + return super.insert(...arguments); + } + static _insert() { /** @type {import("models").Failure} */ - const failure = super.insert(data); + const failure = super._insert(...arguments); if (failure.notifications.length === 0) { failure.delete(); } else { diff --git a/addons/mail/static/src/core/common/follower_model.js b/addons/mail/static/src/core/common/follower_model.js index 284f64cdea0..36d203256eb 100644 --- a/addons/mail/static/src/core/common/follower_model.js +++ b/addons/mail/static/src/core/common/follower_model.js @@ -12,7 +12,7 @@ export class Follower extends Record { } /** @returns {import("models").Follower} */ static insert(data) { - return super.insert(data); + return super.insert(...arguments); } followedThread = Record.one("Thread"); diff --git a/addons/mail/static/src/core/common/link_preview_model.js b/addons/mail/static/src/core/common/link_preview_model.js index f8576e8b905..8181cf48f20 100644 --- a/addons/mail/static/src/core/common/link_preview_model.js +++ b/addons/mail/static/src/core/common/link_preview_model.js @@ -10,7 +10,7 @@ export class LinkPreview extends Record { } /** @returns {import("models").LinkPreview} */ static insert(data) { - return super.insert(data); + return super.insert(...arguments); } /** @type {number} */ diff --git a/addons/mail/static/src/core/common/mail_core_common_service.js b/addons/mail/static/src/core/common/mail_core_common_service.js index da16ffe924f..371a0bd8ec3 100644 --- a/addons/mail/static/src/core/common/mail_core_common_service.js +++ b/addons/mail/static/src/core/common/mail_core_common_service.js @@ -2,7 +2,7 @@ import { removeFromArrayWithPredicate } from "@mail/utils/common/arrays"; -import { markup, reactive } from "@odoo/owl"; +import { reactive } from "@odoo/owl"; import { registry } from "@web/core/registry"; @@ -55,14 +55,16 @@ export class MailCoreCommon { }); this.busService.subscribe("mail.message/notification_update", (payload) => { payload.elements.map((message) => { - this.store.Message.insert({ - ...message, - body: markup(message.body), - // implicit: failures are sent by the server at - // initialization only if the current partner is - // author of the message - author: this.store.self, - }); + this.store.Message.insert( + { + ...message, + // implicit: failures are sent by the server at + // initialization only if the current partner is + // author of the message + author: this.store.self, + }, + { html: true } + ); }); }); this.busService.subscribe("mail.message/toggle_star", (payload) => { @@ -105,10 +107,7 @@ export class MailCoreCommon { } const { Message: messageData } = payload; if (messageData) { - this.store.Message.insert({ - ...messageData, - body: messageData.body ? markup(messageData.body) : messageData.body, - }); + this.store.Message.insert(messageData, { html: true }); } }); }); diff --git a/addons/mail/static/src/core/common/message_model.js b/addons/mail/static/src/core/common/message_model.js index e1bd11bbccf..0544bb70a84 100644 --- a/addons/mail/static/src/core/common/message_model.js +++ b/addons/mail/static/src/core/common/message_model.js @@ -35,13 +35,16 @@ export class Message extends Record { } /** @returns {import("models").Message} */ static insert(data) { + return super.insert(...arguments); + } + static _insert(data) { if (data.res_id) { this.store.Thread.insert({ model: data.model, id: data.res_id, }); } - return super.insert(data); + return super._insert(...arguments); } /** @param {Object} data */ @@ -89,7 +92,7 @@ export class Message extends Record { attachments = Record.many("Attachment", { inverse: "message" }); author = Record.one("Persona"); - body = Record.attr(""); + body = Record.attr("", { html: true }); composer = Record.one("Composer", { inverse: "message", onDelete: (r) => r.delete() }); /** @type {string} */ default_subject; diff --git a/addons/mail/static/src/core/common/message_reactions_model.js b/addons/mail/static/src/core/common/message_reactions_model.js index c5e517bdd13..864f8f7665f 100644 --- a/addons/mail/static/src/core/common/message_reactions_model.js +++ b/addons/mail/static/src/core/common/message_reactions_model.js @@ -10,7 +10,7 @@ export class MessageReactions extends Record { } /** @returns {import("models").MessageReactions} */ static insert(data) { - return super.insert(data); + return super.insert(...arguments); } /** @type {string} */ diff --git a/addons/mail/static/src/core/common/message_service.js b/addons/mail/static/src/core/common/message_service.js index d520d7f32b6..0315812d457 100644 --- a/addons/mail/static/src/core/common/message_service.js +++ b/addons/mail/static/src/core/common/message_service.js @@ -3,8 +3,6 @@ import { removeFromArrayWithPredicate } from "@mail/utils/common/arrays"; import { convertBrToLineBreak, prettifyMessageContent } from "@mail/utils/common/format"; -import { markup } from "@odoo/owl"; - import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; @@ -47,11 +45,7 @@ export class MessageService { message_id: message.id, partner_ids: validMentions?.partners?.map((partner) => partner.id), }); - this.store.Message.insert( - Object.assign(messageData, { - body: messageData.body ? markup(messageData.body) : messageData.body, - }) - ); + this.store.Message.insert(messageData, { html: true }); if (!message.isEmpty && this.store.hasLinkPreviewFeature) { this.rpc( "/mail/link_preview", @@ -119,27 +113,6 @@ export class MessageService { return validMentions; } - /** - * Create a transient message, i.e. a message which does not come - * from a member of the channel. Usually a log message, such as one - * generated from a command with ('/'). - * - * @param {Object} data - */ - createTransient(data) { - const { body, res_id, model } = data; - const lastMessageId = this.getLastMessageId(); - return this.store.Message.insert({ - author: this.store.odoobot, - body, - id: lastMessageId + 0.01, - is_note: true, - is_transient: true, - res_id, - model, - }); - } - async toggleStar(message) { await this.orm.silent.call("mail.message", "toggle_message_starred", [[message.id]]); } diff --git a/addons/mail/static/src/core/common/notification_model.js b/addons/mail/static/src/core/common/notification_model.js index 687a5064f0e..f00bd4c4b9c 100644 --- a/addons/mail/static/src/core/common/notification_model.js +++ b/addons/mail/static/src/core/common/notification_model.js @@ -14,7 +14,7 @@ export class Notification extends Record { } /** @returns {import("models").Notification} */ static insert(data) { - return super.insert(data); + return super.insert(...arguments); } /** @type {number} */ diff --git a/addons/mail/static/src/core/common/persona_model.js b/addons/mail/static/src/core/common/persona_model.js index 618f2fb9300..f0804eb6504 100644 --- a/addons/mail/static/src/core/common/persona_model.js +++ b/addons/mail/static/src/core/common/persona_model.js @@ -22,7 +22,7 @@ export class Persona extends Record { } /** @returns {import("models").Persona} */ static insert(data) { - return super.insert(data); + return super.insert(...arguments); } update(data) { diff --git a/addons/mail/static/src/core/common/record.js b/addons/mail/static/src/core/common/record.js index 3771b8045a1..a28c4369327 100644 --- a/addons/mail/static/src/core/common/record.js +++ b/addons/mail/static/src/core/common/record.js @@ -443,6 +443,8 @@ export class RecordList extends Array { * @property {boolean} [MANY_SYM] true when this is a many relation. * @property {boolean} [ONE_SYM] true when this is a one relation. * @property {any} [default] the default value of this attribute. + * @property {boolean} [html] whether the attribute is an html field. Useful to automatically markup + * when the insert is trusted. * @property {string} [targetModel] model name of records contained in this relational field. * @property {Function} [compute] if set the field is computed based on provided function. * The `this` of function is the record, and the function is recalled whenever any field @@ -455,6 +457,15 @@ export class RecordList extends Array { */ export class Record { + /** @param {FieldDefinition} */ + static isAttr(definition) { + return Boolean(definition[ATTR_SYM]); + } + /** + * Determines whether the inserts are considered trusted or not. + * Useful to auto-markup html fields when this is set + */ + static trusted = false; static id; /** @type {Object} */ static records = {}; @@ -465,7 +476,7 @@ export class Record { * - key : field name * - value: Value contains definition of field * - * @type {Object.} + * @type {Object.} */ static _fields = {}; static isRecord(record) { @@ -611,10 +622,10 @@ export class Record { */ static new(data) { const obj = new this.Class(); + obj.Model = this; const ids = this._retrieveIdFromData(data); let record = Object.assign(obj, { [IS_RECORD_SYM]: true, - Model: this, localId: this.localId(data), ...ids, }); @@ -662,13 +673,23 @@ export class Record { /** * @template T * @param {T} def; + * @param {boolean} [html] if set, the field value contains html value. + * Useful to automatically markup when the insert is trusted. * @returns {T} */ - static attr(def) { - return [ATTR_SYM, { default: def }]; + static attr(def, { html } = {}) { + return [ATTR_SYM, { default: def, html }]; } /** @returns {Record} */ - static insert(data) { + static insert(data, { html } = {}) { + const oldTrusted = Record.trusted; + Record.trusted = html ?? Record.trusted; + const res = this._insert(...arguments); + Record.trusted = oldTrusted; + return res; + } + /** @returns {Record} */ + static _insert(data) { const res = this.preinsert(data); res.update(data); return res; @@ -808,8 +829,10 @@ export class Record { for (const [name, val] of Object.entries(this._fields)) { if (RecordList.isMany(val)) { data[name] = val.map((r) => r.toIdData()); - } else { + } else if (RecordList.isOne(val)) { data[name] = this[name]?.toIdData(); + } else { + data[name] = this[name]; // Record.attr() } } delete data._store; diff --git a/addons/mail/static/src/core/common/store_service.js b/addons/mail/static/src/core/common/store_service.js index 0df2e31e09b..350aeb1d608 100644 --- a/addons/mail/static/src/core/common/store_service.js +++ b/addons/mail/static/src/core/common/store_service.js @@ -2,12 +2,18 @@ import { onChange } from "@mail/utils/common/misc"; -import { reactive } from "@odoo/owl"; +import { markup, reactive } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { debounce } from "@web/core/utils/timing"; import { modelRegistry, Record, RecordUses, RecordList } from "./record"; +/** + * Class of markup, useful to detect content that is markup and to + * automatically markup field during trusted insert + */ +const Markup = markup("").constructor; + export class BaseStore extends Record { /** * @param {string} localId @@ -25,7 +31,7 @@ export class BaseStore extends Record { export class Store extends BaseStore { /** @returns {import("models").Store} */ static insert() { - return super.insert(); + return super.insert(...arguments); } /** @type {typeof import("@mail/core/web/activity_model").Activity} */ @@ -172,10 +178,23 @@ export function makeStore(env) { }, /** @param {Record} receiver */ set(target, name, val, receiver) { - if (!(name in receiver._fields)) { + if (name === "Model" || !(name in receiver.Model._fields)) { Reflect.set(target, name, val, receiver); return true; } + if (Record.isAttr(receiver.Model._fields[name])) { + if ( + receiver.Model._fields[name].html && + Record.trusted && + typeof val === "string" && + !(val instanceof Markup) + ) { + Reflect.set(target, name, markup(val), receiver); + } else { + Reflect.set(target, name, val, receiver); + } + return true; + } /** @type {RecordList} */ const l1 = receiver._fields[name]; if (RecordList.isMany(l1)) { diff --git a/addons/mail/static/src/core/common/thread_model.js b/addons/mail/static/src/core/common/thread_model.js index f450d96aad0..ce9e03cb37c 100644 --- a/addons/mail/static/src/core/common/thread_model.js +++ b/addons/mail/static/src/core/common/thread_model.js @@ -61,7 +61,7 @@ export class Thread extends Record { if (!("model" in data)) { throw new Error("Cannot insert thread: model is missing in data"); } - return super.insert(data); + return super.insert(...arguments); } /** @param {Object} data */ diff --git a/addons/mail/static/src/core/common/thread_service.js b/addons/mail/static/src/core/common/thread_service.js index deddeb4bf08..a0ba7828e84 100644 --- a/addons/mail/static/src/core/common/thread_service.js +++ b/addons/mail/static/src/core/common/thread_service.js @@ -4,8 +4,6 @@ import { loadEmoji } from "@web/core/emoji_picker/emoji_picker"; import { DEFAULT_AVATAR } from "@mail/core/common/persona_service"; import { prettifyMessageContent } from "@mail/utils/common/format"; -import { markup } from "@odoo/owl"; - import { browser } from "@web/core/browser/browser"; import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; @@ -193,16 +191,9 @@ export class ThreadService { after, before, }); - const messages = rawMessages.reverse().map((data) => { - if (data.parentMessage) { - data.parentMessage.body = data.parentMessage.body - ? markup(data.parentMessage.body) - : data.parentMessage.body; - } - return this.store.Message.insert( - Object.assign(data, { body: data.body ? markup(data.body) : data.body }) - ); - }); + const messages = rawMessages + .reverse() + .map((data) => this.store.Message.insert(data, { html: true })); thread.isLoaded = true; return messages; } catch (e) { @@ -301,15 +292,9 @@ export class ThreadService { ...this.getFetchParams(thread), around: messageId, }); - thread.messages = messages.reverse().map((message) => { - if (message.parentMessage?.body) { - message.parentMessage.body = markup(message.parentMessage.body); - } - return this.store.Message.insert({ - ...message, - body: message.body ? markup(message.body) : message.body, - }); - }); + thread.messages = messages + .reverse() + .map((message) => this.store.Message.insert(message, { html: true })); thread.loadNewer = messageId ? true : false; thread.loadOlder = true; if (messages.length < FETCH_LIMIT) { @@ -338,14 +323,7 @@ export class ThreadService { const previews = await this.orm.call("discuss.channel", "channel_fetch_preview", [ids]); for (const preview of previews) { const thread = this.store.Thread.get({ model: "discuss.channel", id: preview.id }); - const data = Object.assign(preview.last_message, { - body: markup(preview.last_message.body), - }); - const message = this.store.Message.insert({ - ...data, - res_id: thread.id, - model: thread.model, - }); + const message = this.store.Message.insert(preview.last_message, { html: true }); if (!thread.isLoaded) { thread.messages.push(message); if (message.isNeedaction) { @@ -718,13 +696,16 @@ export class ThreadService { } } browser.localStorage.setItem("web.emoji.frequent", JSON.stringify(recentEmojis)); - tmpMsg = this.store.Message.insert({ - ...tmpData, - body: markup(prettyContent), - res_id: thread.id, - model: thread.model, - temporary_id: tmpId, - }); + tmpMsg = this.store.Message.insert( + { + ...tmpData, + body: prettyContent, + res_id: thread.id, + model: thread.model, + temporary_id: tmpId, + }, + { html: true } + ); thread.messages.push(tmpMsg); thread.seen_message_id = tmpMsg.id; } @@ -733,15 +714,10 @@ export class ThreadService { if (!data) { return; } - if (data.parentMessage) { - data.parentMessage.body = data.parentMessage.body - ? markup(data.parentMessage.body) - : data.parentMessage.body; - } if (data.id in this.store.Message.records) { data.temporary_id = null; } - const message = this.store.Message.insert(Object.assign(data, { body: markup(data.body) })); + const message = this.store.Message.insert(data, { html: true }); thread.messages.add(message); if (!message.isEmpty && this.store.hasLinkPreviewFeature) { this.rpc("/mail/link_preview", { message_id: data.id }, { silent: true }); @@ -992,15 +968,7 @@ export class ThreadService { return { count, loadMore: messages.length === FETCH_LIMIT, - messages: messages.map((message) => { - message.body = markup(message.body); - if (message.parentMessage) { - message.parentMessage.body = message.parentMessage.body - ? markup(message.parentMessage.body) - : message.parentMessage.body; - } - return this.store.Message.insert(message); - }), + messages: messages.map((message) => this.store.Message.insert(message, { html: true })), }; } } diff --git a/addons/mail/static/src/core/web/activity_model.js b/addons/mail/static/src/core/web/activity_model.js index 083f8a0401b..617c476f1af 100644 --- a/addons/mail/static/src/core/web/activity_model.js +++ b/addons/mail/static/src/core/web/activity_model.js @@ -49,6 +49,15 @@ export class Activity extends Record { * @returns {import("models").Activity} */ static insert(data, { broadcast = true } = {}) { + return super.insert(...arguments); + } + /** + * @param {Data} data + * @param {Object} [param1] + * @param {boolean} param1.broadcast + * @returns {import("models").Activity} + */ + static _insert(data, { broadcast = true } = {}) { /** @type {import("models").Activity} */ const activity = this.preinsert(data); if (data.request_partner_id) { @@ -101,8 +110,7 @@ export class Activity extends Record { id; /** @type {Object[]} */ mail_template_ids; - /** @type {string} */ - note; + note = Record.attr("", { html: true }); /** @type {number|false} */ previous_activity_type_id; /** @type {number|false} */ diff --git a/addons/mail/static/src/core/web/mail_core_web_service.js b/addons/mail/static/src/core/web/mail_core_web_service.js index 906db00257b..d522f6f10fe 100644 --- a/addons/mail/static/src/core/web/mail_core_web_service.js +++ b/addons/mail/static/src/core/web/mail_core_web_service.js @@ -2,7 +2,7 @@ import { removeFromArray, removeFromArrayWithPredicate } from "@mail/utils/common/arrays"; -import { markup, reactive } from "@odoo/owl"; +import { reactive } from "@odoo/owl"; import { registry } from "@web/core/registry"; @@ -24,14 +24,16 @@ export class MailCoreWeb { this.messagingService.isReady.then(() => { this.rpc("/mail/load_message_failures", {}, { silent: true }).then((messages) => { messages.map((messageData) => - this.store.Message.insert({ - ...messageData, - body: messageData.body ? markup(messageData.body) : messageData.body, - // implicit: failures are sent by the server at - // initialization only if the current partner is - // author of the message - author: this.store.user, - }) + this.store.Message.insert( + { + ...messageData, + // implicit: failures are sent by the server at + // initialization only if the current partner is + // author of the message + author: this.store.user, + }, + { html: true } + ) ); this.store.failures.sort((f1, f2) => f2.lastMessage?.id - f1.lastMessage?.id); }); @@ -52,8 +54,7 @@ export class MailCoreWeb { } }); this.busService.subscribe("mail.message/inbox", (payload) => { - const data = Object.assign(payload, { body: markup(payload.body) }); - const message = this.store.Message.insert(data); + const message = this.store.Message.insert(payload, { html: true }); const inbox = this.store.discuss.inbox; if (message.notIn(inbox.messages)) { inbox.counter++; diff --git a/addons/mail/static/src/core/web/thread_service_patch.js b/addons/mail/static/src/core/web/thread_service_patch.js index 204d37f7717..fdd7327ad47 100644 --- a/addons/mail/static/src/core/web/thread_service_patch.js +++ b/addons/mail/static/src/core/web/thread_service_patch.js @@ -3,8 +3,6 @@ import { ThreadService, threadService } from "@mail/core/common/thread_service"; import { parseEmail } from "@mail/js/utils"; -import { markup } from "@odoo/owl"; - import { _t } from "@web/core/l10n/translation"; import { patch } from "@web/core/utils/patch"; @@ -51,10 +49,7 @@ patch(ThreadService.prototype, { if ("activities" in result) { const existingIds = new Set(); for (const activity of result.activities) { - if (activity.note) { - activity.note = markup(activity.note); - } - existingIds.add(this.store.Activity.insert(activity).id); + existingIds.add(this.store.Activity.insert(activity, { html: true }).id); } for (const activity of thread.activities) { if (!existingIds.has(activity.id)) { diff --git a/addons/mail/static/src/discuss/call/common/rtc_session_model.js b/addons/mail/static/src/discuss/call/common/rtc_session_model.js index 961062c2072..e3a1f73acfe 100644 --- a/addons/mail/static/src/discuss/call/common/rtc_session_model.js +++ b/addons/mail/static/src/discuss/call/common/rtc_session_model.js @@ -12,8 +12,11 @@ export class RtcSession extends Record { } /** @returns {number, import("models").RtcSession} */ static insert(data) { + return super.insert(...arguments); + } + static _insert() { /** @type {import("models").RtcSession} */ - const session = super.insert(data); + const session = super._insert(...arguments); session.channel?.rtcSessions.add(session); return session; } diff --git a/addons/mail/static/src/discuss/core/common/discuss_core_common_service.js b/addons/mail/static/src/discuss/core/common/discuss_core_common_service.js index a24b4da85ec..fb6493cdefb 100644 --- a/addons/mail/static/src/discuss/core/common/discuss_core_common_service.js +++ b/addons/mail/static/src/discuss/core/common/discuss_core_common_service.js @@ -1,6 +1,6 @@ /* @odoo-module */ -import { markup, reactive } from "@odoo/owl"; +import { reactive } from "@odoo/owl"; import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; @@ -84,8 +84,19 @@ export class DiscussCoreCommon { model: "discuss.channel", id: payload.res_id, }); - const message = this.messageService.createTransient( - Object.assign(payload, { body: markup(payload.body) }) + const { body, res_id, model } = payload; + const lastMessageId = this.messageService.getLastMessageId(); + const message = this.store.Message.insert( + { + author: this.store.odoobot, + body, + id: lastMessageId + 0.01, + is_note: true, + is_transient: true, + res_id, + model, + }, + { html: true } ); channel.messages.push(message); channel.transientMessages.push(message); @@ -204,17 +215,7 @@ export class DiscussCoreCommon { } this.store.Message.get(messageData.temporary_id)?.delete(); messageData.temporary_id = null; - if ("parentMessage" in messageData && messageData.parentMessage.body) { - messageData.parentMessage.body = markup(messageData.parentMessage.body); - } - const data = Object.assign(messageData, { - body: markup(messageData.body), - }); - const message = this.store.Message.insert({ - ...data, - res_id: channel.id, - model: channel.model, - }); + const message = this.store.Message.insert(messageData, { html: true }); if (message.notIn(channel.messages)) { if (!channel.loadNewer) { channel.messages.push(message); diff --git a/addons/mail/static/src/discuss/message_pin/common/message_pin_service.js b/addons/mail/static/src/discuss/message_pin/common/message_pin_service.js index 7c4d3e691d2..7d1913a03c4 100644 --- a/addons/mail/static/src/discuss/message_pin/common/message_pin_service.js +++ b/addons/mail/static/src/discuss/message_pin/common/message_pin_service.js @@ -6,7 +6,7 @@ import { Message as MessageModel } from "@mail/core/common/message_model"; import { Record } from "@mail/core/common/record"; import { Thread } from "@mail/core/common/thread_model"; -import { markup, reactive } from "@odoo/owl"; +import { reactive } from "@odoo/owl"; import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; @@ -80,13 +80,9 @@ export class MessagePin { const messagesData = await this.rpcService("/discuss/channel/pinned_messages", { channel_id: channel.id, }); - messagesData.forEach((messageData) => { - if (messageData.parentMessage) { - messageData.parentMessage.body = markup(messageData.parentMessage.body); - } - messageData.body = markup(messageData.body); - this.store.Message.insert(messageData); - }); + messagesData.forEach((messageData) => + this.store.Message.insert(messageData, { html: true }) + ); channel.pinnedMessagesState = "loaded"; } catch (e) { channel.pinnedMessagesState = "error"; diff --git a/addons/mail/static/tests/core/record_tests.js b/addons/mail/static/tests/core/record_tests.js index 36b61571621..73067f4d86b 100644 --- a/addons/mail/static/tests/core/record_tests.js +++ b/addons/mail/static/tests/core/record_tests.js @@ -5,6 +5,7 @@ import { BaseStore, makeStore } from "@mail/core/common/store_service"; import { registry } from "@web/core/registry"; import { clearRegistryWithCleanup, makeTestEnv } from "@web/../tests/helpers/mock_env"; +import { markup } from "@odoo/owl"; const serviceRegistry = registry.category("services"); @@ -138,3 +139,16 @@ QUnit.test("Computed relational field", async (assert) => { thread.members.delete(john); assert.ok(thread.admin.eq(marc)); }); + +QUnit.test("Trusted insert on html field with { html: true }", async (assert) => { + (class Message extends Record { + static id = "body"; + body = Record.attr("", { html: true }); + }).register(); + const store = await start(); + const hello = store.Message.insert("

hello

", { html: true }); + const world = store.Message.insert("

world

"); + assert.ok(hello.body instanceof markup("").constructor); + assert.strictEqual(hello.body.toString(), "

hello

"); + assert.strictEqual(world.body, "

world

"); +}); diff --git a/addons/test_mail_full/static/tests/messaging_menu_patch_tests.js b/addons/test_mail_full/static/tests/messaging_menu_patch_tests.js index e69e25861d6..f73be741143 100644 --- a/addons/test_mail_full/static/tests/messaging_menu_patch_tests.js +++ b/addons/test_mail_full/static/tests/messaging_menu_patch_tests.js @@ -12,7 +12,7 @@ QUnit.test("rating value displayed on the preview", async (assert) => { const partnerId = pyEnv["res.partner"].create({}); const channelId = pyEnv["discuss.channel"].create({}); const messageId = pyEnv["mail.message"].create([ - { author_id: partnerId, model: "discuss.channel", res_id: channelId }, + { author_id: partnerId, body: "non-empty", model: "discuss.channel", res_id: channelId }, ]); pyEnv["rating.rating"].create({ consumed: true,