From 69f911d994a07e73e8d54afc2845be6b63f8b2bc Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Thu, 2 Feb 2023 17:15:50 +0000 Subject: [PATCH] [IMP] *: enforce usage of Markup in mail When using message_post, the body format must be explicitly specified. If html is expected, a Markup object should be used. If text is given, the content will be escaped. Before this PR: message_post was unaware if the content of a message was HTML or text. This lead to multiple situation where the content was incorrectly considered as HTML and led to display errors. In self.message_post(body="Hello %s!" % self.name) if the name contained HTML, it would be evaluated. In self.message_post(body="Contact Raoul ") the email would not be displayed as considered as unknown HTML and discarded by the sanitizer Now each call must explict the type of content. Use the escape() helper to properly combine Markup and translations. It would also be acceptable to use Markup() to wrap a static translation but escape is better as one can not guarantee the content of a translation. closes odoo/odoo#111850 Related: odoo/documentation#3612 Related: odoo/enterprise#36728 Signed-off-by: Thibault Delavallee (tde) --- addons/account/models/account_move.py | 11 ++- addons/account/models/account_move_line.py | 6 +- addons/account/models/account_payment.py | 13 +-- addons/account/models/chart_template.py | 3 +- addons/account/models/res_partner_bank.py | 7 +- .../wizard/account_automatic_entry_wizard.py | 79 ++++++++++++------- addons/account/wizard/accrued_orders.py | 13 +-- .../wizard/account_debit_note.py | 7 +- .../models/account_edi_common.py | 28 ++++--- addons/account_fleet/models/account_move.py | 6 +- .../models/payment_transaction.py | 8 +- addons/crm/models/crm_lead.py | 21 ++--- addons/crm/models/crm_team.py | 5 +- addons/crm/wizard/crm_lead_lost.py | 3 +- addons/crm_livechat/models/mail_channel.py | 6 +- .../models/event_booth_registration.py | 5 +- .../models/gamification_challenge.py | 11 +-- addons/google_calendar/models/google_sync.py | 8 +- addons/hr/models/hr_employee.py | 6 +- addons/hr/wizard/hr_plan_wizard.py | 8 +- addons/hr_expense/models/account_move.py | 3 +- addons/hr_expense/models/account_payment.py | 3 +- .../wizard/survey_invite.py | 9 ++- .../hr_skills_slides/models/slide_channel.py | 20 +++-- addons/im_livechat/tests/test_message.py | 4 +- .../im_livechat_mail_bot/models/mail_bot.py | 8 +- .../l10n_in_edi/models/account_edi_format.py | 9 ++- .../models/account_edi_format.py | 7 +- addons/l10n_it_edi/models/account_invoice.py | 3 +- addons/mail/controllers/discuss.py | 3 + addons/mail/models/mail_channel.py | 17 ++-- addons/mail/models/mail_thread.py | 10 +-- addons/mail/tests/test_res_partner.py | 3 +- addons/mail_bot/models/mail_bot.py | 26 +++--- addons/mail_bot/models/res_users.py | 8 +- addons/maintenance/models/maintenance.py | 6 +- addons/mass_mailing/models/mailing.py | 4 +- .../wizard/mailing_mailing_test.py | 9 +-- .../wizard/mailing_sms_test.py | 5 +- addons/mrp/models/mrp_unbuild.py | 13 ++- addons/payment/models/payment_transaction.py | 5 +- addons/point_of_sale/models/pos_order.py | 8 +- addons/point_of_sale/models/pos_session.py | 5 +- addons/purchase/models/account_invoice.py | 5 +- addons/sale/models/sale_order.py | 15 ++-- addons/sale/models/sale_order_line.py | 13 +-- addons/sale_project/models/sale_order_line.py | 8 +- addons/stock/models/stock_move.py | 3 +- addons/stock/models/stock_picking.py | 3 +- addons/stock_delivery/models/stock_picking.py | 19 ++--- .../models/stock_picking.py | 4 +- .../models/stock_picking_batch.py | 4 +- addons/test_mail/tests/test_mail_followers.py | 4 +- addons/test_mail/tests/test_mail_mail.py | 5 +- addons/test_mail/tests/test_mail_message.py | 3 +- .../tests/test_mail_thread_internals.py | 5 +- addons/test_mail/tests/test_message_post.py | 42 +++++----- addons/test_mail/tests/test_performance.py | 18 ++--- .../tests/test_mail_performance.py | 3 +- .../models/crm_lead.py | 11 +-- .../models/payment_transaction.py | 5 +- 61 files changed, 344 insertions(+), 268 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index d93340606df..6585b3ac91b 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -7,6 +7,7 @@ from dateutil.relativedelta import relativedelta from hashlib import sha256 from json import dumps import logging +from markupsafe import Markup, escape from psycopg2 import OperationalError import re from textwrap import shorten @@ -2232,12 +2233,10 @@ class AccountMove(models.Model): default['partner_id'] = False copied_am = super().copy(default) message_origin = '' if not copied_am.auto_post_origin_id else \ - '
' + _('This recurring entry originated from %s', copied_am.auto_post_origin_id._get_html_link()) - copied_am._message_log(body=_( - 'This entry has been duplicated from %s%s', - self._get_html_link(), - message_origin, - )) + (Markup('
') + _('This recurring entry originated from %s')) % copied_am.auto_post_origin_id._get_html_link() + copied_am._message_log(body= + (escape(_('This entry has been duplicated from %s')) % self._get_html_link()) + message_origin, + ) return copied_am diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index b108b0536d3..b95c8a935ca 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -3,6 +3,7 @@ from collections import defaultdict from contextlib import contextmanager from datetime import date, timedelta from functools import lru_cache +from markupsafe import escape from odoo import api, fields, models, Command, _ from odoo.exceptions import ValidationError, UserError @@ -1448,10 +1449,7 @@ class AccountMoveLine(models.Model): for line in self.filtered(lambda l: l.move_id.id == move_id): tracking_value_ids = line._mail_track(ref_fields, modified_lines)[1] if tracking_value_ids: - msg = _( - "Journal Item %s updated", - line._get_html_link(title=f"#{line.id}") - ) + msg = escape(_("Journal Item %s updated")) % line._get_html_link(title=f"#{line.id}") line.move_id._message_log( body=msg, tracking_value_ids=tracking_value_ids diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 960de318d93..c91d49dbfd7 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from markupsafe import escape + from odoo import models, fields, api, _, Command from odoo.exceptions import UserError, ValidationError from odoo.tools.misc import format_date, formatLang @@ -896,16 +898,9 @@ class AccountPayment(models.Model): }) paired_payment.move_id._post(soft=False) payment.paired_internal_transfer_payment_id = paired_payment - - body = _( - "This payment has been created from %s", - payment._get_html_link(), - ) + body = escape(_("This payment has been created from:")) + payment._get_html_link() paired_payment.message_post(body=body) - body = _( - "A second payment has been created: %s", - paired_payment._get_html_link(), - ) + body = escape(_("A second payment has been created:")) + paired_payment._get_html_link() payment.message_post(body=body) lines = (payment.move_id.line_ids + paired_payment.move_id.line_ids).filtered( diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 2b95a3d3e75..31b955e243d 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- import ast -from collections import defaultdict import csv +from collections import defaultdict from functools import wraps from inspect import getmembers + import logging import re diff --git a/addons/account/models/res_partner_bank.py b/addons/account/models/res_partner_bank.py index 94e78f15b45..5081e530f4b 100644 --- a/addons/account/models/res_partner_bank.py +++ b/addons/account/models/res_partner_bank.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import base64 from collections import defaultdict +from markupsafe import escape import werkzeug import werkzeug.exceptions @@ -241,7 +242,7 @@ class ResPartnerBank(models.Model): # EXTENDS base res.partner.bank res = super().create(vals_list) for account in res: - msg = _("Bank Account %s created", account._get_html_link(title=f"#{account.id}")) + msg = escape(_("Bank Account %s created")) % account._get_html_link(title=f"#{account.id}") account.partner_id._message_log(body=msg) return res @@ -269,7 +270,7 @@ class ResPartnerBank(models.Model): for account, initial_values in account_initial_values.items(): tracking_value_ids = account._mail_track(fields_definition, initial_values)[1] if tracking_value_ids: - msg = _("Bank Account %s updated", account._get_html_link(title=f"#{account.id}")) + msg = escape(_("Bank Account %s updated")) % account._get_html_link(title=f"#{account.id}") account.partner_id._message_log(body=msg, tracking_value_ids=tracking_value_ids) if 'partner_id' in initial_values: # notify previous partner as well initial_values['partner_id']._message_log(body=msg, tracking_value_ids=tracking_value_ids) @@ -278,7 +279,7 @@ class ResPartnerBank(models.Model): def unlink(self): # EXTENDS base res.partner.bank for account in self: - msg = _("Bank Account %s with number %s deleted", account._get_html_link(title=f"#{account.id}"), account.acc_number) + msg = escape(_("Bank Account %s with number %s deleted")) % (account._get_html_link(title=f"#{account.id}"), account.acc_number) account.partner_id._message_log(body=msg) return super().unlink() diff --git a/addons/account/wizard/account_automatic_entry_wizard.py b/addons/account/wizard/account_automatic_entry_wizard.py index 0d1a221692a..d4bc48a0fc4 100644 --- a/addons/account/wizard/account_automatic_entry_wizard.py +++ b/addons/account/wizard/account_automatic_entry_wizard.py @@ -2,9 +2,11 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError from odoo.tools.misc import format_date, formatLang +from odoo.tools.float_utils import float_repr from collections import defaultdict from itertools import groupby +from markupsafe import Markup, escape import json class AutomaticEntryWizard(models.TransientModel): @@ -249,7 +251,7 @@ class AutomaticEntryWizard(models.TransientModel): 'analytic_distribution': aml.analytic_distribution, }), (0, 0, { - 'name': self._format_strings(_('{percent:0.2f}% recognized on {new_date}'), aml.move_id), + 'name': self._format_strings(_('{percent}% recognized on {new_date}'), aml.move_id), 'debit': reported_credit, 'credit': reported_debit, 'amount_currency': -reported_amount_currency, @@ -271,7 +273,7 @@ class AutomaticEntryWizard(models.TransientModel): 'analytic_distribution': aml.analytic_distribution, }), (0, 0, { - 'name': self._format_strings(_('{percent:0.2f}% to recognize on {new_date}'), aml.move_id), + 'name': self._format_strings(_('{percent}% to recognize on {new_date}'), aml.move_id), 'debit': reported_debit, 'credit': reported_credit, 'amount_currency': reported_amount_currency, @@ -350,17 +352,30 @@ class AutomaticEntryWizard(models.TransientModel): accrual_move_lines = accrual_move.mapped('line_ids').filtered(lambda line: line.account_id == accrual_account)[accrual_move_offsets[accrual_move]:accrual_move_offsets[accrual_move]+2] accrual_move_offsets[accrual_move] += 2 (accrual_move_lines + destination_move_lines).filtered(lambda line: not line.currency_id.is_zero(line.balance)).reconcile() - move.message_post(body=self._format_strings(_('Adjusting Entries have been created for this invoice:
  • %(link1)s cancelling ' - '{percent:.2f}%% of {amount}
  • %(link0)s postponing it to {new_date}
', - link0=self._format_move_link(destination_move), - link1=self._format_move_link(accrual_move), - ), move, amount)) - destination_messages += [self._format_strings(_('Adjusting Entry {link}: {percent:.2f}% of {amount} recognized from {date}'), move, amount)] - accrual_move_messages[accrual_move] += [self._format_strings(_('Adjusting Entry for {link}: {percent:.2f}% of {amount} recognized on {new_date}'), move, amount)] + body = Markup("%(title)s
  • %(link1)s %(second)s
  • %(link2)s %(third)s
") % { + 'title': _("Adjusting Entries have been created for this invoice:"), + 'link1': self._format_move_link(accrual_move), + 'second': self._format_strings(_("cancelling {percent}%% of {amount}"), move, amount), + 'link2': self._format_move_link(destination_move), + 'third': self._format_strings(_("postponing it to {new_date}"), move, amount), + } + move.message_post(body=body) + destination_messages += [ + self._format_strings( + escape(_("Adjusting Entry {link} {percent}%% of {amount} recognized from {date}")), + move, amount, + ) + ] + accrual_move_messages[accrual_move] += [ + self._format_strings( + escape(_("Adjusting Entry {link} {percent}%% of {amount} recognized on {new_date}")), + move, amount, + ) + ] - destination_move.message_post(body='
\n'.join(destination_messages)) + destination_move.message_post(body=Markup('
\n').join(destination_messages)) for accrual_move, messages in accrual_move_messages.items(): - accrual_move.message_post(body='
\n'.join(messages)) + accrual_move.message_post(body=Markup('
\n').join(messages)) # open the generated entries action = { @@ -418,31 +433,41 @@ class AutomaticEntryWizard(models.TransientModel): # Transfer utils def _format_new_transfer_move_log(self, acc_transfer_per_move): - format = _("
  • {amount} ({debit_credit}) from {link}, %(account_source_name)s
  • ") - rslt = _("This entry transfers the following amounts to %(destination)s
      ", destination=self.destination_account_id.display_name) - for move, balances_per_account in acc_transfer_per_move.items(): - for account, balance in balances_per_account.items(): - if account != self.destination_account_id: # Otherwise, logging it here is confusing for the user - rslt += self._format_strings(format, move, balance) % {'account_source_name': account.display_name} - - rslt += '
    ' + transfer_format = Markup("
  • %s, %%(account_source_name)s
  • ") % \ + _("{amount} ({debit_credit}) from {link}") + rslt = Markup(_("This entry transfers the following amounts to %(destination)s") + "
      %(transfer_logs)s
    ") % { + 'destination': Markup("%s") % self.destination_account_id.display_name, + 'transfer_logs': Markup().join([ + self._format_strings(transfer_format, move, balance) % {'account_source_name': account.display_name} + for move, balances_per_account in acc_transfer_per_move.items() + for account, balance in balances_per_account.items() + if account != self.destination_account_id # Otherwise, logging it here is confusing for the user + ]) + } return rslt def _format_transfer_source_log(self, balances_per_account, transfer_move): - transfer_format = _("
  • {amount} ({debit_credit}) from %s were transferred to {account_target_name} by {link}
  • ") - content = '' - for account, balance in balances_per_account.items(): - if account != self.destination_account_id: - content += self._format_strings(transfer_format, transfer_move, balance) % account.display_name - return content and '
      ' + content + '
    ' or None + if not balances_per_account: + return None + + transfer_format = Markup( + _("{amount} ({debit_credit}) from %s were transferred to {account_target_name} by {link}") + ) + + return Markup("
      %s
    ") % Markup().join([ + Markup("
  • %s
  • ") % \ + self._format_strings(transfer_format, transfer_move, balance) % (Markup("%s") % account.display_name) + for account, balance in balances_per_account.items() + if account != self.destination_account_id + ]) def _format_move_link(self, move): return move._get_html_link() def _format_strings(self, string, move, amount=None): return string.format( - label=move.name or 'Adjusting Entry', - percent=self.percentage, + label=move.name or _('Adjusting Entry'), + percent=float_repr(self.percentage, 2), name=move.name, id=move.id, amount=formatLang(self.env, abs(amount), currency_obj=self.company_id.currency_id) if amount else '', diff --git a/addons/account/wizard/accrued_orders.py b/addons/account/wizard/accrued_orders.py index ca0f258b90e..d61c6048114 100644 --- a/addons/account/wizard/accrued_orders.py +++ b/addons/account/wizard/accrued_orders.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from dateutil.relativedelta import relativedelta +from markupsafe import escape import json from odoo import models, fields, api, _, Command from odoo.tools import format_date @@ -217,13 +218,13 @@ class AccruedExpenseRevenue(models.TransientModel): }]) reverse_move._post() for order in orders_with_entries: - body = _( + body = escape(_( 'Accrual entry created on %(date)s: %(accrual_entry)s.\ - And its reverse entry: %(reverse_entry)s.', - date=self.date, - accrual_entry=move._get_html_link(), - reverse_entry=reverse_move._get_html_link(), - ) + And its reverse entry: %(reverse_entry)s.')) % { + 'date': self.date, + 'accrual_entry': move._get_html_link(), + 'reverse_entry': reverse_move._get_html_link(), + } order.message_post(body=body) return { 'name': _('Accrual Moves'), diff --git a/addons/account_debit_note/wizard/account_debit_note.py b/addons/account_debit_note/wizard/account_debit_note.py index b03addb8d1b..3982de5d622 100644 --- a/addons/account_debit_note/wizard/account_debit_note.py +++ b/addons/account_debit_note/wizard/account_debit_note.py @@ -2,7 +2,7 @@ from odoo import models, fields, api from odoo.tools.translate import _ from odoo.exceptions import UserError - +from markupsafe import escape class AccountDebitNote(models.TransientModel): """ @@ -71,10 +71,7 @@ class AccountDebitNote(models.TransientModel): for move in self.move_ids.with_context(include_business_fields=True): #copy sale/purchase links default_values = self._prepare_default_values(move) new_move = move.copy(default=default_values) - move_msg = _( - "This debit note was created from: %s", - move._get_html_link(), - ) + move_msg = escape(_("This debit note was created from: %s")) % move._get_html_link() new_move.message_post(body=move_msg) new_moves |= new_move diff --git a/addons/account_edi_ubl_cii/models/account_edi_common.py b/addons/account_edi_ubl_cii/models/account_edi_common.py index 7db5988c2ea..2de56f97579 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_common.py +++ b/addons/account_edi_ubl_cii/models/account_edi_common.py @@ -5,6 +5,7 @@ from odoo.tools import float_repr from odoo.exceptions import UserError, ValidationError from odoo.tools.float_utils import float_round +from markupsafe import Markup from zeep import Client # ------------------------------------------------------------------------- @@ -285,13 +286,14 @@ class AccountEdiCommon(models.AbstractModel): invoice.move_type = move_type logs = self._import_fill_invoice_form(invoice, tree, qty_factor) if invoice: + body = Markup("%s") % \ + _("Format used to import the invoice: %s", + self.env['ir.model']._get(self._name).name) + if logs: - body = _( - "Format used to import the invoice: %s

  • %s
  • ", - str(self._description), "
  • ".join(logs) - ) - else: - body = _("Format used to import the invoice: %s", str(self._description)) + body += Markup("
      %s
    ") % \ + Markup().join(Markup("
  • %s
  • ") % l for l in logs) + invoice.message_post(body=body) # For UBL, we should override the computed tax amount if it is less than 0.05 different of the one in the xml. @@ -656,22 +658,24 @@ class AccountEdiCommon(models.AbstractModel): for item in response['Result']: if item['artifactPath']: report.append( - "
  • " + item['artifactPath'] + "
  • ") + Markup("
  • %s
  • ") % item['artifactPath']) for detail in item['Item']: if detail['errorLevel'] == 'WARN': errors_cnt += 1 report.append( - "
  • " + detail['errorText'] + "
  • ") + Markup("
  • %s
  • ") % detail['errorText']) elif detail['errorLevel'] == 'ERROR': errors_cnt += 1 report.append( - "
  • " + detail['errorText'] + "
  • ") + Markup("
  • %s
  • ") % detail['errorText']) if errors_cnt == 0: - invoice.message_post(body=f"ECOSIO: All clear for format {ecosio_format}!") + invoice.message_post(body=Markup("ECOSIO: All clear for format %s!") % ecosio_format) else: invoice.message_post( - body=f"ECOSIO ERRORS/WARNINGS for format {ecosio_format}:
      " - + "\n".join(report) + "
    " + body=Markup("ECOSIO ERRORS/WARNINGS for format %s:
      %s") % ( + ecosio_format, + Markup().join(report) + ) ) return response diff --git a/addons/account_fleet/models/account_move.py b/addons/account_fleet/models/account_move.py index d0f12d08da7..e4a2314c136 100644 --- a/addons/account_fleet/models/account_move.py +++ b/addons/account_fleet/models/account_move.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import escape from odoo import models, fields, api, _ @@ -18,10 +19,7 @@ class AccountMove(models.Model): posted = super()._post(soft) # We need the move name to be set, but we also need to know which move are posted for the first time. for line in (not_posted_before & posted).line_ids.filtered(lambda ml: ml.vehicle_id and ml.move_id.move_type == 'in_invoice'): val = line._prepare_fleet_log_service() - log = _( - 'Service Vendor Bill: %s', - line.move_id._get_html_link(), - ) + log = escape(_('Service Vendor Bill: %s')) % line.move_id._get_html_link() val_list.append(val) log_list.append(log) log_service_ids = self.env['fleet.vehicle.log.services'].create(val_list) diff --git a/addons/account_payment/models/payment_transaction.py b/addons/account_payment/models/payment_transaction.py index 776973de8a3..da6e68093e2 100644 --- a/addons/account_payment/models/payment_transaction.py +++ b/addons/account_payment/models/payment_transaction.py @@ -1,5 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import escape from odoo import api, fields, models, SUPERUSER_ID, _ @@ -214,8 +215,7 @@ class PaymentTransaction(models.Model): """ super()._finalize_post_processing() for tx in self.filtered('payment_id'): - message = _( - "The payment related to the transaction with reference %(ref)s has been posted: " - "%(link)s", ref=tx.reference, link=tx.payment_id._get_html_link() - ) + message = escape(_("The payment related to the transaction with reference %(ref)s has been posted: %(link)s")) % { + 'ref':tx.reference, 'link':tx.payment_id._get_html_link() + } tx._log_message_on_linked_documents(message) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index e57f58cf522..85dba55598c 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -7,7 +7,7 @@ import threading from ast import literal_eval from collections import OrderedDict, defaultdict from datetime import date, datetime, timedelta -from markupsafe import Markup +from markupsafe import Markup, escape from psycopg2 import sql from odoo import api, fields, models, tools, SUPERUSER_ID @@ -1306,15 +1306,16 @@ class Lead(models.Model): duration = _('unknown') else: duration = self.env['ir.qweb.field.duration'].value_to_html(meeting.duration, {'unit': 'hour'}) - meet_date = fields.Datetime.from_string(meeting.start) - meeting_usertime = fields.Datetime.to_string(fields.Datetime.context_timestamp(self, meet_date)) - message = "

      %s

      " % Markup(_("Meeting scheduled at %(html_time)s
      Subject: %(subject)s
      Duration: %(duration)s")) % { - 'html_time': Markup("") % { - 'meeting_start': meeting.start, - 'meeting_user_time': meeting_usertime, - }, - 'subject': meeting._get_html_link(), # Already Markup valid - 'duration': duration, + meeting_usertime = fields.Datetime.to_string(fields.Datetime.context_timestamp(self, meeting.start)) + meeting_time = Markup("") % { + 'meeting_start': meeting.start, + 'meeting_user_time': meeting_usertime, + } + message = Markup("

      %(meeting)s
      %(subject_string)s %(subject_link)s
      %(duration)s

      ") % { + 'meeting': escape(_("Meeting scheduled at %s")) % meeting_time, + 'subject_string': _("Subject: "), + 'subject_link': meeting._get_html_link(), + 'duration': _("Duration: %s", duration), } return self.message_post(body=message) diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py index 5dae56e7b90..1223a4b7d90 100644 --- a/addons/crm/models/crm_team.py +++ b/addons/crm/models/crm_team.py @@ -7,6 +7,7 @@ import random import threading from ast import literal_eval +from markupsafe import Markup from odoo import api, exceptions, fields, models, _ from odoo.osv import expression @@ -263,13 +264,13 @@ class Team(models.Model): # format result messages logs = self._action_assign_leads_logs(teams_data, members_data) - html_message = '
      '.join(logs) + html_message = Markup('
      ').join(logs) notif_message = ' '.join(logs) # log a note in case of manual assign (as this method will mainly be called # on singleton record set, do not bother doing a specific message per team) log_action = _("Lead Assignment requested by %(user_name)s", user_name=self.env.user.name) - log_message = "

      %s

      %s

      " % (log_action, html_message) + log_message = Markup("

      %s

      %s

      ") % (log_action, html_message) self._message_log_batch(bodies=dict((team.id, log_message) for team in self)) return { diff --git a/addons/crm/wizard/crm_lead_lost.py b/addons/crm/wizard/crm_lead_lost.py index 149ce8336cb..694483dd90c 100644 --- a/addons/crm/wizard/crm_lead_lost.py +++ b/addons/crm/wizard/crm_lead_lost.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup from odoo import fields, models, _ from odoo.tools.mail import is_html_empty @@ -19,7 +20,7 @@ class CrmLeadLost(models.TransientModel): leads = self.env['crm.lead'].browse(self.env.context.get('active_ids')) if not is_html_empty(self.lost_feedback): leads._track_set_log_message( - '

      %s:

      %s
      ' % ( + Markup('

      %s:

      %s
      ') % ( _('Lost Comment'), self.lost_feedback ) diff --git a/addons/crm_livechat/models/mail_channel.py b/addons/crm_livechat/models/mail_channel.py index 51d3e905d9a..46c4604d65e 100644 --- a/addons/crm_livechat/models/mail_channel.py +++ b/addons/crm_livechat/models/mail_channel.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import escape from odoo import models, _ from odoo.tools import html2plaintext @@ -15,10 +16,7 @@ class MailChannel(models.Model): msg = _('Create a new lead (/lead lead title)') else: lead = self._convert_visitor_to_lead(partner, key) - msg = _( - 'Created a new lead: %s', - lead._get_html_link(), - ) + msg = escape(_('Created a new lead: %s')) % lead._get_html_link() self._send_transient_message(partner, msg) def _convert_visitor_to_lead(self, partner, key): diff --git a/addons/event_booth_sale/models/event_booth_registration.py b/addons/event_booth_sale/models/event_booth_registration.py index 60c1c949e35..4d7c6995517 100644 --- a/addons/event_booth_sale/models/event_booth_registration.py +++ b/addons/event_booth_sale/models/event_booth_registration.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup from odoo import api, fields, models, _ @@ -61,9 +62,9 @@ class EventBoothRegistration(models.Model): self._cancel_pending_registrations() def _cancel_pending_registrations(self): - body = '

      %(message)s:

        %(booth_names)s

      ' % { + body = Markup('

      %(message)s:

        %(booth_names)s

      ') % { 'message': _('Your order has been cancelled because the following booths have been reserved'), - 'booth_names': ''.join('
    • %s
    • ' % booth.display_name for booth in self.event_booth_id) + 'booth_names': Markup().join(Markup('
    • %s
    • ') % booth.display_name for booth in self.event_booth_id) } other_registrations = self.search([ ('event_booth_id', 'in', self.event_booth_id.ids), diff --git a/addons/gamification/models/gamification_challenge.py b/addons/gamification/models/gamification_challenge.py index b9f6ead1015..1c4fd0dcd38 100644 --- a/addons/gamification/models/gamification_challenge.py +++ b/addons/gamification/models/gamification_challenge.py @@ -6,6 +6,7 @@ import logging from datetime import date, timedelta from dateutil.relativedelta import relativedelta, MO +from markupsafe import Markup from odoo import api, models, fields, _, exceptions from odoo.tools import ustr @@ -694,21 +695,21 @@ class Challenge(models.Model): if rewarded_users: user_names = rewarded_users.name_get() - message_body += _( - "
      Reward (badge %(badge_name)s) for every succeeding user was sent to %(users)s.", + message_body += Markup("
      ") + _( + "Reward (badge %(badge_name)s) for every succeeding user was sent to %(users)s.", badge_name=challenge.reward_id.name, users=", ".join(name for (user_id, name) in user_names) ) else: - message_body += _("
      Nobody has succeeded to reach every goal, no badge is rewarded for this challenge.") + message_body += Markup("
      ") + _("Nobody has succeeded to reach every goal, no badge is rewarded for this challenge.") # reward bests - reward_message = _("
      %(rank)d. %(user_name)s - %(reward_name)s") + reward_message = Markup("
      %(rank)d. %(user_name)s - %(reward_name)s") if challenge.reward_first_id: (first_user, second_user, third_user) = challenge._get_topN_users(MAX_VISIBILITY_RANKING) if first_user: challenge._reward_user(first_user, challenge.reward_first_id) - message_body += _("
      Special rewards were sent to the top competing users. The ranking for this challenge is:") + message_body += Markup("
      ") + _("Special rewards were sent to the top competing users. The ranking for this challenge is:") message_body += reward_message % { 'rank': 1, 'user_name': first_user.name, diff --git a/addons/google_calendar/models/google_sync.py b/addons/google_calendar/models/google_sync.py index da97e3151a3..17c8b7dbbe6 100644 --- a/addons/google_calendar/models/google_sync.py +++ b/addons/google_calendar/models/google_sync.py @@ -7,6 +7,7 @@ from functools import wraps from requests import HTTPError import pytz from dateutil.parser import parse +from markupsafe import Markup from odoo import api, fields, models, registry, _ from odoo.tools import ormcache_context, email_normalize @@ -210,10 +211,9 @@ class GoogleSync(models.AbstractModel): 'reason': reason} _logger.error(error_log) - body = _( - "The following event could not be synced with Google Calendar.
      " - "It will not be synced as long at it is not updated.
      " - "%(reason)s", reason=reason) + body = _("The following event could not be synced with Google Calendar.") + Markup("
      ") + \ + _("It will not be synced as long at it is not updated.") + Markup("
      ") + \ + reason if event: event.message_post( diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index 28b4ee3d217..9a03ec1ed09 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -8,6 +8,7 @@ from random import choice from string import digits from werkzeug.urls import url_encode from dateutil.relativedelta import relativedelta +from markupsafe import Markup from odoo import api, fields, models, _ from odoo.exceptions import ValidationError, AccessError @@ -367,10 +368,9 @@ class HrEmployeePrivate(models.Model): 'active_model': 'hr.employee', 'menu_id': hr_root_menu.id, }) - onboarding_notes_bodies[employee.id] = _( + onboarding_notes_bodies[employee.id] = Markup(_( 'Congratulations! May I recommend you to setup an onboarding plan?', - url, - ) + )) % url employees._message_log_batch(onboarding_notes_bodies) return employees diff --git a/addons/hr/wizard/hr_plan_wizard.py b/addons/hr/wizard/hr_plan_wizard.py index 82ce4f4e1fc..c15063d05a4 100644 --- a/addons/hr/wizard/hr_plan_wizard.py +++ b/addons/hr/wizard/hr_plan_wizard.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup + from odoo import api, fields, models, _ from odoo.exceptions import ValidationError @@ -94,10 +96,10 @@ class HrPlanWizard(models.TransientModel): activities.add(activity) if activities: - body += '
        ' + body += Markup('
          ') for activity in activities: - body += '
        • %s
        • ' % activity - body += '
        ' + body += Markup('
      • %s
      • ') % activity + body += Markup('
      ') employee.message_post(body=body) if len(self.employee_ids) == 1: diff --git a/addons/hr_expense/models/account_move.py b/addons/hr_expense/models/account_move.py index 45f40b5edfc..875c5726e26 100644 --- a/addons/hr_expense/models/account_move.py +++ b/addons/hr_expense/models/account_move.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict +from markupsafe import escape from odoo import models, fields, api, _ from odoo.tools.misc import frozendict @@ -39,7 +40,7 @@ class AccountMove(models.Model): def _creation_message(self): if self.expense_sheet_id: - return _("Expense entry created from: %s", self.expense_sheet_id._get_html_link()) + return escape(_("Expense entry created from: %s")) % self.expense_sheet_id._get_html_link() return super()._creation_message() @api.depends('expense_sheet_id') diff --git a/addons/hr_expense/models/account_payment.py b/addons/hr_expense/models/account_payment.py index a16a8af66e5..6f338878d79 100644 --- a/addons/hr_expense/models/account_payment.py +++ b/addons/hr_expense/models/account_payment.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import escape from odoo import models, _ from odoo.exceptions import UserError @@ -38,5 +39,5 @@ class AccountPayment(models.Model): # EXTENDS mail self.ensure_one() if self.move_id.expense_sheet_id: - return _("Payment created for: %s", self.move_id.expense_sheet_id._get_html_link()) + return escape(_("Payment created for: %s")) % self.move_id.expense_sheet_id._get_html_link() return super()._creation_message() diff --git a/addons/hr_recruitment_survey/wizard/survey_invite.py b/addons/hr_recruitment_survey/wizard/survey_invite.py index c24d2d151c6..d55fe391a02 100644 --- a/addons/hr_recruitment_survey/wizard/survey_invite.py +++ b/addons/hr_recruitment_survey/wizard/survey_invite.py @@ -1,5 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup, escape + from odoo import fields, models, _ from odoo.tools.misc import clean_context @@ -36,7 +38,10 @@ class SurveyInvite(models.TransientModel): partner = self.applicant_id.partner_id survey_link = survey._get_html_link(title=survey.title) partner_link = partner._get_html_link() - content = _('The survey %(survey_link)s has been sent to %(partner_link)s', survey_link=survey_link, partner_link=partner_link) - body = '

      %s

      ' % content + content = escape(_('The survey %(survey_link)s has been sent to %(partner_link)s')) % { + 'survey_link': survey_link, + 'partner_link': partner_link, + } + body = Markup('

      %s

      ') % content self.applicant_id.message_post(body=body) return super().action_invite() diff --git a/addons/hr_skills_slides/models/slide_channel.py b/addons/hr_skills_slides/models/slide_channel.py index 48f56b240cf..ecfdb40ed93 100644 --- a/addons/hr_skills_slides/models/slide_channel.py +++ b/addons/hr_skills_slides/models/slide_channel.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup, escape + from odoo import fields, models, _ from odoo.tools import html2plaintext @@ -48,9 +50,11 @@ class SlideChannelPartner(models.Model): super()._send_completed_mail() for scp in self: if self.env.user.employee_ids: - msg = _('The employee has completed the course %(course)s', - link=scp.channel_id.website_url, - course=scp.channel_id.name) + msg = escape(_('The employee has completed the course %s')) % \ + Markup('%(course)s') % { + 'link': scp.channel_id.website_url, + 'course': scp.channel_id.name, + } self.env.user.employee_id.message_post(body=msg) class Channel(models.Model): @@ -60,7 +64,10 @@ class Channel(models.Model): res = super()._action_add_members(target_partners) for channel in self: channel._message_employee_chatter( - _('The employee subscribed to the course %(course)s', link=channel.website_url, course=channel.name), + Markup(_('The employee subscribed to the course %(course)s')) % { + 'link': channel.website_url, + 'course': channel.name, + }, target_partners) return res @@ -71,7 +78,10 @@ class Channel(models.Model): for channel in self: channel._message_employee_chatter( - _('The employee left the course %(course)s', link=channel.website_url, course=channel.name), + Markup(_('The employee left the course %(course)s')) % { + 'link': channel.website_url, + 'course': channel.name, + }, partners) return res diff --git a/addons/im_livechat/tests/test_message.py b/addons/im_livechat/tests/test_message.py index 16dd79de8e4..57c2d560946 100644 --- a/addons/im_livechat/tests/test_message.py +++ b/addons/im_livechat/tests/test_message.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup + from odoo import Command from odoo.tests.common import users, tagged, TransactionCase @@ -39,7 +41,7 @@ class TestImLivechatMessage(TransactionCase): }) message = channel_livechat_1.message_post( author_id=record_rating.partner_id.id, - body=":%s/5%s" + body=Markup(":%s/5%s") % (record_rating.rating_image_url, record_rating.rating, record_rating.feedback), rating_id=record_rating.id, ) diff --git a/addons/im_livechat_mail_bot/models/mail_bot.py b/addons/im_livechat_mail_bot/models/mail_bot.py index a17e1cac292..f892f24a645 100644 --- a/addons/im_livechat_mail_bot/models/mail_bot.py +++ b/addons/im_livechat_mail_bot/models/mail_bot.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup, escape from odoo import models, _ @@ -13,13 +14,14 @@ class MailBot(models.AbstractModel): if odoobot_state == "onboarding_attachement" and values.get("attachment_ids"): self.env.user.odoobot_failed = False self.env.user.odoobot_state = "onboarding_canned" - return _("That's me! 🎉
      Try typing : to use canned responses.") + return Markup(_("That's me! 🎉
      Try typing %s to use canned responses.", ":")) elif odoobot_state == "onboarding_canned" and values.get("canned_response_ids"): self.env.user.odoobot_failed = False self.env.user.odoobot_state = "idle" - return _("Good, you can customize canned responses in the live chat application.

      It's the end of this overview, enjoy discovering Odoo!") + return Markup(_("Good, you can customize canned responses in the live chat application.

      It's the end of this overview, enjoy discovering Odoo!")) # repeat question if needed elif odoobot_state == 'onboarding_canned' and not self._is_help_requested(body): self.env.user.odoobot_failed = True - return _("Not sure what you are doing. Please, type : and wait for the propositions. Select one of them and press enter.") + return escape(_("Not sure what you are doing. Please, type %s and wait for the propositions. Select one of them and press enter.")) % \ + Markup(":") return super(MailBot, self)._get_answer(record, body, values, command) diff --git a/addons/l10n_in_edi/models/account_edi_format.py b/addons/l10n_in_edi/models/account_edi_format.py index 4cf4e0ebb4d..ce67dcb1a8d 100644 --- a/addons/l10n_in_edi/models/account_edi_format.py +++ b/addons/l10n_in_edi/models/account_edi_format.py @@ -7,6 +7,7 @@ import pytz import markupsafe from collections import defaultdict +from markupsafe import Markup from odoo import models, fields, api, _ from odoo.tools import html_escape, float_is_zero, float_compare @@ -125,12 +126,12 @@ class AccountEdiFormat(models.Model): if not response.get("error"): error = [] odoobot = self.env.ref("base.partner_root") - invoice.message_post(author_id=odoobot.id, body=_( + invoice.message_post(author_id=odoobot.id, body=Markup(_( "Somehow this invoice had been submited to government before." \ "
      Normally, this should not happen too often" \ "
      Just verify value of invoice by uploade json to government website " \ "here." - )) + ))) if "no-credit" in error_codes: return {invoice: { "success": False, @@ -180,12 +181,12 @@ class AccountEdiFormat(models.Model): if "9999" in error_codes: response = {} odoobot = self.env.ref("base.partner_root") - invoice.message_post(author_id=odoobot.id, body=_( + invoice.message_post(author_id=odoobot.id, body=Markup(_( "Somehow this invoice had been cancelled to government before." \ "
      Normally, this should not happen too often" \ "
      Just verify by logging into government website " \ "
      here." - )) + ))) if "no-credit" in error_codes: return {invoice: { "success": False, diff --git a/addons/l10n_in_edi_ewaybill/models/account_edi_format.py b/addons/l10n_in_edi_ewaybill/models/account_edi_format.py index e003497d4d8..572d17c6baa 100755 --- a/addons/l10n_in_edi_ewaybill/models/account_edi_format.py +++ b/addons/l10n_in_edi_ewaybill/models/account_edi_format.py @@ -4,6 +4,7 @@ import re import json from datetime import timedelta +from markupsafe import Markup from odoo import models, fields, api, _ from odoo.tools import html_escape @@ -150,12 +151,12 @@ class AccountEdiFormat(models.Model): if "312" in error_codes: # E-waybill is already canceled # this happens when timeout from the Government portal but IRN is generated - error_message = "
      ".join(["[%s] %s" % (e.get("code"), html_escape(e.get("message") or self._l10n_in_edi_ewaybill_get_error_message(e.get('code')))) for e in error]) + error_message = Markup("
      ").join([Markup("[%s] %s") % (e.get("code"), e.get("message") or self._l10n_in_edi_ewaybill_get_error_message(e.get('code'))) for e in error]) error = [] response = {"data": ""} odoobot = self.env.ref("base.partner_root") invoices.message_post(author_id=odoobot.id, body= - "%s
      %s:
      %s" %( + Markup("%s
      %s:
      %s") %( _("Somehow this E-waybill has been canceled in the government portal before. You can verify by checking the details into the government (https://ewaybillgst.gov.in/Others/EBPrintnew.asp)"), _("Error"), error_message @@ -168,7 +169,7 @@ class AccountEdiFormat(models.Model): "blocking_level": "error", } elif error: - error_message = "
      ".join(["[%s] %s" % (e.get("code"), html_escape(e.get("message") or self._l10n_in_edi_ewaybill_get_error_message(e.get('code')))) for e in error]) + error_message = Markup("
      ").join([Markup("[%s] %s") % (e.get("code"), e.get("message") or self._l10n_in_edi_ewaybill_get_error_message(e.get('code'))) for e in error]) blocking_level = "error" if "404" in error_codes: blocking_level = "warning" diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py index 6a0418e87c9..10e53b67d21 100644 --- a/addons/l10n_it_edi/models/account_invoice.py +++ b/addons/l10n_it_edi/models/account_invoice.py @@ -7,6 +7,7 @@ import logging import re from datetime import datetime +from markupsafe import escape from odoo import api, fields, models, _ from odoo.tools import float_repr, float_compare @@ -352,7 +353,7 @@ class AccountMove(models.Model): }) with other_invoice._get_edi_creation() as other_invoice: self.env['account.edi.format']._import_fattura_pa(tree, other_invoice) - other_invoice.message_post(body=_("Created from attachment in %s", invoice._get_html_link())) + other_invoice.message_post(body=escape(_("Created from attachment in %s")) % invoice._get_html_link()) return True diff --git a/addons/mail/controllers/discuss.py b/addons/mail/controllers/discuss.py index 182786efcf3..5c66bd2f939 100644 --- a/addons/mail/controllers/discuss.py +++ b/addons/mail/controllers/discuss.py @@ -3,6 +3,7 @@ from collections import defaultdict from datetime import datetime, timedelta +from markupsafe import Markup from psycopg2 import IntegrityError from psycopg2.errorcodes import UNIQUE_VIOLATION @@ -238,6 +239,8 @@ class DiscussController(http.Controller): thread = channel_member_sudo.channel_id else: thread = request.env[thread_model].browse(int(thread_id)).exists() + if 'body' in post_data: + post_data['body'] = Markup(post_data['body']) # contains HTML such as @mentions message_data = thread.message_post(**{key: value for key, value in post_data.items() if key in self._get_allowed_message_post_params()}).message_format()[0] if 'temporary_id' in request.context: message_data['temporary_id'] = request.context['temporary_id'] diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index a8a66035d45..f93efd63670 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -5,6 +5,7 @@ import base64 import logging from collections import defaultdict from hashlib import sha512 +from markupsafe import Markup from secrets import choice from markupsafe import Markup @@ -315,7 +316,7 @@ class Channel(models.Model): # channel_info is called before actually unpinning the channel channel_info['is_pinned'] = False self.env['bus.bus']._sendone(partner, 'mail.channel/leave', channel_info) - notification = _('
      left the channel
      ') + notification = Markup('
      %s
      ') % _('left the channel') # post 'channel left' message as root since the partner just unsubscribed from the channel self.sudo().message_post(body=notification, subtype_xmlid="mail.mt_comment", author_id=partner.id) self.env['bus.bus']._sendone(self, 'mail.record/insert', { @@ -387,15 +388,13 @@ class Channel(models.Model): if post_joined_message: # notify existing members with a new message in the channel if member.partner_id == self.env.user.partner_id: - notification = _('
      joined the channel
      ') + notification = Markup('
      %s
      ') % _('joined the channel') else: - notification = _( - '
      invited %s to the channel
      ', - member.partner_id._get_html_link(), - ) + notification = (Markup('
      %s
      ') % _("invited %s to the channel")) % member.partner_id._get_html_link() member.channel_id.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment") for member in new_members.filtered(lambda member: member.guest_id): - member.channel_id.message_post(body=_('
      joined the channel
      '), message_type="notification", subtype_xmlid="mail.mt_comment") + member.channel_id.message_post(body=Markup('
      %s
      ') % _('joined the channel'), + message_type="notification", subtype_xmlid="mail.mt_comment") guest = member.guest_id if guest: notifications.append((guest, 'mail.channel/joined', { @@ -1135,7 +1134,9 @@ class Channel(models.Model): new_channel = self.create(vals) group = self.env['res.groups'].search([('id', '=', group_id)]) if group_id else None new_channel.group_public_id = group.id if group else None - notification = _('
      ', new_channel.id, new_channel.name) + notification = (Markup('
      %s
      ') % _("created %(link)s")) % { + 'link': Markup('#%s') % (new_channel.id, new_channel.name) + } new_channel.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment") channel_info = new_channel.channel_info()[0] self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.channel/legacy_insert', channel_info) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 219294838c8..99cd2c801bf 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -23,7 +23,7 @@ from email import message_from_string from lxml import etree from werkzeug import urls from xmlrpc import client as xmlrpclib -from markupsafe import Markup +from markupsafe import Markup, escape from odoo import _, api, exceptions, fields, models, tools, registry, SUPERUSER_ID, Command from odoo.exceptions import MissingError, AccessError @@ -1353,7 +1353,7 @@ class MailThread(models.AbstractModel): for node in to_remove: node.getparent().remove(node) if postprocessed: - body = etree.tostring(root, pretty_print=False, encoding='unicode') + body = Markup(etree.tostring(root, pretty_print=False, encoding='unicode')) return {'body': body, 'attachments': attachments} def _message_parse_extract_payload(self, message, message_dict, save_original=False): @@ -2023,7 +2023,7 @@ class MailThread(models.AbstractModel): 'model': self._name, 'res_id': self.id, # content - 'body': body, + 'body': escape(body), # escape if text, keep if markup 'message_type': message_type, 'parent_id': self._message_compute_parent_id(parent_id), 'subject': subject or False, @@ -2485,7 +2485,7 @@ class MailThread(models.AbstractModel): 'record_name': False, 'res_id': self.id if self else res_id, # content - 'body': body, + 'body': escape(body), # escape if text, keep if markup 'is_internal': True, 'message_type': 'user_notification', 'subject': subject, @@ -2601,7 +2601,7 @@ class MailThread(models.AbstractModel): values_list = [dict(base_message_values, res_id=record.id, - body=bodies.get(record.id, '')) + body=escape(bodies.get(record.id, ''))) for record in self] return self.sudo()._message_create(values_list) diff --git a/addons/mail/tests/test_res_partner.py b/addons/mail/tests/test_res_partner.py index 0590df65092..9096320df00 100644 --- a/addons/mail/tests/test_res_partner.py +++ b/addons/mail/tests/test_res_partner.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from contextlib import contextmanager +from markupsafe import Markup from unittest.mock import patch from uuid import uuid4 @@ -407,7 +408,7 @@ class TestPartner(MailCommon): p1.message_subscribe(partner_ids=p3.ids) p1_act1 = p1.activity_schedule(act_type_xmlid='mail.mail_activity_data_todo') p1_msg1 = p1.message_post( - body='

      Log on P1

      ', + body=Markup('

      Log on P1

      '), subtype_id=self.env.ref('mail.mt_comment').id ) self.assertEqual(p1.activity_ids, p1_act1) diff --git a/addons/mail_bot/models/mail_bot.py b/addons/mail_bot/models/mail_bot.py index 50ad2db6154..97344eb242c 100644 --- a/addons/mail_bot/models/mail_bot.py +++ b/addons/mail_bot/models/mail_bot.py @@ -4,9 +4,9 @@ import itertools import random +from markupsafe import Markup from odoo import models, _ - class MailBot(models.AbstractModel): _name = 'mail.bot' _description = 'Mail Bot' @@ -40,19 +40,19 @@ class MailBot(models.AbstractModel): if odoobot_state == 'onboarding_emoji' and self._body_contains_emoji(body): self.env.user.odoobot_state = "onboarding_command" self.env.user.odoobot_failed = False - return _("Great! 👍
      To access special commands, start your sentence with /. Try getting help.") + return Markup(_("Great! 👍
      To access special commands, start your sentence with /. Try getting help.")) elif odoobot_state == 'onboarding_command' and command == 'help': self.env.user.odoobot_state = "onboarding_ping" self.env.user.odoobot_failed = False - return _("Wow you are a natural!
      Ping someone with @username to grab their attention. Try to ping me using @OdooBot in a sentence.") + return Markup(_("Wow you are a natural!
      Ping someone with @username to grab their attention. Try to ping me using @OdooBot in a sentence.")) elif odoobot_state == 'onboarding_ping' and self._is_bot_pinged(values): self.env.user.odoobot_state = "onboarding_attachement" self.env.user.odoobot_failed = False - return _("Yep, I am here! 🎉
      Now, try sending an attachment, like a picture of your cute dog...") + return Markup(_("Yep, I am here! 🎉
      Now, try sending an attachment, like a picture of your cute dog...")) elif odoobot_state == 'onboarding_attachement' and values.get("attachment_ids"): self.env.user.odoobot_state = "idle" self.env.user.odoobot_failed = False - return _("I am a simple bot, but if that's a dog, he is the cutest 😊
      Congratulations, you finished this tour. You can now close this chat window. Enjoy discovering Odoo.") + return Markup(_("I am a simple bot, but if that's a dog, he is the cutest 😊
      Congratulations, you finished this tour. You can now close this chat window. Enjoy discovering Odoo.")) elif odoobot_state in (False, "idle", "not_initialized") and (_('start the tour') in body.lower()): self.env.user.odoobot_state = "onboarding_emoji" return _("To start, try to send me an emoji :)") @@ -63,28 +63,28 @@ class MailBot(models.AbstractModel): return _("That's not nice! I'm a bot but I have feelings... 💔") # help message elif self._is_help_requested(body) or odoobot_state == 'idle': - return _("Unfortunately, I'm just a bot 😞 I don't understand! If you need help discovering our product, please check " + return Markup(_("Unfortunately, I'm just a bot 😞 I don't understand! If you need help discovering our product, please check " "our documentation or " - "our videos.") + "our videos.")) else: # repeat question if odoobot_state == 'onboarding_emoji': self.env.user.odoobot_failed = True - return _("Not exactly. To continue the tour, send an emoji: type :) and press enter.") + return Markup(_("Not exactly. To continue the tour, send an emoji: type :) and press enter.")) elif odoobot_state == 'onboarding_attachement': self.env.user.odoobot_failed = True - return _("To send an attachment, click on the icon and select a file.") + return Markup(_("To send an attachment, click on the icon and select a file.")) elif odoobot_state == 'onboarding_command': self.env.user.odoobot_failed = True - return _("Not sure what you are doing. Please, type / and wait for the propositions. Select help and press enter") + return Markup(_("Not sure what you are doing. Please, type / and wait for the propositions. Select help and press enter")) elif odoobot_state == 'onboarding_ping': self.env.user.odoobot_failed = True - return _("Sorry, I am not listening. To get someone's attention, ping him. Write @OdooBot and select me.") + return Markup(_("Sorry, I am not listening. To get someone's attention, ping him. Write @OdooBot and select me.")) return random.choice([ - _("I'm not smart enough to answer your question.
      To follow my guide, ask: start the tour."), + Markup(_("I'm not smart enough to answer your question.
      To follow my guide, ask: start the tour.")), _("Hmmm..."), _("I'm afraid I don't understand. Sorry!"), - _("Sorry I'm sleepy. Or not! Maybe I'm just trying to hide my unawareness of human language...
      I can show you features if you write: start the tour.") + Markup(_("Sorry I'm sleepy. Or not! Maybe I'm just trying to hide my unawareness of human language...
      I can show you features if you write: start the tour.")) ]) return False diff --git a/addons/mail_bot/models/res_users.py b/addons/mail_bot/models/res_users.py index b9bb935ee71..293c2aa69ec 100644 --- a/addons/mail_bot/models/res_users.py +++ b/addons/mail_bot/models/res_users.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup + from odoo import models, fields, _ class Users(models.Model): @@ -32,7 +34,11 @@ class Users(models.Model): odoobot_id = self.env['ir.model.data']._xmlid_to_res_id("base.partner_root") channel_info = self.env['mail.channel'].channel_get([odoobot_id, self.partner_id.id]) channel = self.env['mail.channel'].browse(channel_info['id']) - message = _("Hello,
      Odoo's chat helps employees collaborate efficiently. I'm here to help you discover its features.
      Try to send me an emoji :)") + message = Markup("%s
      %s
      %s :)") % ( + _("Hello,"), + _("Odoo's chat helps employees collaborate efficiently. I'm here to help you discover its features."), + _("Try to send me an emoji") + ) channel.sudo().message_post(body=message, author_id=odoobot_id, message_type="comment", subtype_xmlid="mail.mt_comment") self.sudo().odoobot_state = 'onboarding_emoji' return channel diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py index 3932e348300..2d5cce39fb5 100644 --- a/addons/maintenance/models/maintenance.py +++ b/addons/maintenance/models/maintenance.py @@ -3,6 +3,7 @@ import ast from datetime import date, datetime, timedelta +from markupsafe import escape from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.exceptions import UserError @@ -386,10 +387,7 @@ class MaintenanceRequest(models.Model): new_user_id=request.user_id.id or request.owner_user_id.id or self.env.uid) if not updated: if request.equipment_id: - note = _( - 'Request planned for %s', - request.equipment_id._get_html_link() - ) + note = escape(_('Request planned for %s')) % request.equipment_id._get_html_link() else: note = False request.activity_schedule( diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py index 91b43a264a4..2ac07c3c815 100644 --- a/addons/mass_mailing/models/mailing.py +++ b/addons/mass_mailing/models/mailing.py @@ -780,8 +780,8 @@ class MassMailing(models.Model): ]) opt_out_records.write({'opt_out': value}) - message = _('The recipient unsubscribed from %s mailing list(s)') \ - if value else _('The recipient subscribed to %s mailing list(s)') + message = Markup(_('The recipient unsubscribed from %s mailing list(s)')) \ + if value else Markup(_('The recipient subscribed to %s mailing list(s)')) for record in records: # filter the list_id by record record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id) diff --git a/addons/mass_mailing/wizard/mailing_mailing_test.py b/addons/mass_mailing/wizard/mailing_mailing_test.py index b00684fb8c6..5df68d198bd 100644 --- a/addons/mass_mailing/wizard/mailing_mailing_test.py +++ b/addons/mass_mailing/wizard/mailing_mailing_test.py @@ -77,17 +77,16 @@ class TestMassMailing(models.TransientModel): _('Test mailing successfully sent to %s', mail_sudo.email_to)) elif mail_sudo.state == 'exception': notification_messages.append( - _('Test mailing could not be sent to %s:
      %s', - mail_sudo.email_to, - mail_sudo.failure_reason) + _('Test mailing could not be sent to %s:', mail_sudo.email_to) + + (Markup("
      ") + mail_sudo.failure_reason) ) # manually delete the emails since we passed 'auto_delete: False' mails_sudo.unlink() if notification_messages: - self.mass_mailing_id._message_log(body='
        %s
      ' % ''.join( - ['
    • %s
    • ' % notification_message for notification_message in notification_messages] + self.mass_mailing_id._message_log(body=Markup('
        %s
      ') % ''.join( + [Markup('
    • %s
    • ') % notification_message for notification_message in notification_messages] )) return True diff --git a/addons/mass_mailing_sms/wizard/mailing_sms_test.py b/addons/mass_mailing_sms/wizard/mailing_sms_test.py index b013ce33df7..44dad036712 100644 --- a/addons/mass_mailing_sms/wizard/mailing_sms_test.py +++ b/addons/mass_mailing_sms/wizard/mailing_sms_test.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup from odoo import fields, models, _ from odoo.addons.phone_validation.tools import phone_validation @@ -59,8 +60,8 @@ class MassSMSTest(models.TransientModel): ) if notification_messages: - self.mailing_id._message_log(body='
        %s
      ' % ''.join( - ['
    • %s
    • ' % notification_message for notification_message in notification_messages] + self.mailing_id._message_log(body=Markup('
        %s
      ') % ''.join( + [Markup('
    • %s
    • ') % notification_message for notification_message in notification_messages] )) return True diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py index 30d62e99607..494c398d27b 100644 --- a/addons/mrp/models/mrp_unbuild.py +++ b/addons/mrp/models/mrp_unbuild.py @@ -7,7 +7,7 @@ from odoo.tools import float_compare, float_round from odoo.osv import expression from collections import defaultdict - +from markupsafe import escape class MrpUnbuild(models.Model): _name = "mrp.unbuild" @@ -202,12 +202,11 @@ class MrpUnbuild(models.Model): produced_move_line_ids = produce_moves.mapped('move_line_ids').filtered(lambda ml: ml.qty_done > 0) consume_moves.mapped('move_line_ids').write({'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]}) if self.mo_id: - unbuild_msg = _( - "%(qty)s %(measure)s unbuilt in %(order)s", - qty=self.product_qty, - measure=self.product_uom_id.name, - order=self._get_html_link(), - ) + unbuild_msg = escape(_("%(qty)s %(measure)s unbuilt in %(order)s")) % { + 'qty': self.product_qty, + 'measure': self.product_uom_id.name, + 'order': self._get_html_link(), + } self.mo_id.message_post( body=unbuild_msg, subtype_xmlid='mail.mt_note', diff --git a/addons/payment/models/payment_transaction.py b/addons/payment/models/payment_transaction.py index 477d317f85d..ec18267b583 100644 --- a/addons/payment/models/payment_transaction.py +++ b/addons/payment/models/payment_transaction.py @@ -8,6 +8,7 @@ from datetime import datetime import psycopg2 from dateutil import relativedelta +from markupsafe import Markup from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError @@ -1109,7 +1110,7 @@ class PaymentTransaction(models.Model): ref=self.reference, amount=formatted_amount, provider_name=self.provider_id.name ) if self.state_message: - message += "
      " + _("Error: %s", self.state_message) + message += Markup("
      ") + _("Error: %s", self.state_message) else: message = _( ("The transaction with reference %(ref)s for %(amount)s is canceled " @@ -1119,7 +1120,7 @@ class PaymentTransaction(models.Model): provider_name=self.provider_id.name ) if self.state_message: - message += "
      " + _("Reason: %s", self.state_message) + message += Markup("
      ") + _("Reason: %s", self.state_message) return message def _get_last(self): diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 06f7d92c4ad..746f4843834 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -6,6 +6,7 @@ from markupsafe import Markup from functools import partial from itertools import groupby from collections import defaultdict +from markupsafe import escape import psycopg2 import pytz @@ -501,10 +502,9 @@ class PosOrder(models.Model): def _create_invoice(self, move_vals): self.ensure_one() new_move = self.env['account.move'].sudo().with_company(self.company_id).with_context(default_move_type=move_vals['move_type']).create(move_vals) - message = _( - "This invoice has been created from the point of sale session: %s", - self._get_html_link(), - ) + message = escape(_("This invoice has been created from the point of sale session: %s")) % \ + self._get_html_link() + new_move.message_post(body=message) if self.config_id.cash_rounding: rounding_applied = float_round(self.amount_paid - self.amount_total, diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 851d712cf21..fef7720f3f9 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -4,6 +4,7 @@ from collections import defaultdict from datetime import timedelta from itertools import groupby +from markupsafe import Markup from odoo import api, fields, models, _, Command from odoo.exceptions import AccessError, UserError, ValidationError @@ -1500,7 +1501,7 @@ class PosSession(models.Model): f"{self.currency_id.round(difference)} " \ f"{self.currency_id.symbol if self.currency_id.position == 'after' else ''}
      " if notes: - message += notes.replace('\n', '
      ') + message += notes.replace('\n', Markup('
      ')) if message: self.message_post(body=message) @@ -1563,7 +1564,7 @@ class PosSession(models.Model): message_content = [f"Cash {extras['translatedType']}", f'- Amount: {extras["formattedAmount"]}'] if reason: message_content.append(f'- Reason: {reason}') - self.message_post(body='
      \n'.join(message_content)) + self.message_post(body=Markup('
      \n').join(message_content)) def get_onboarding_data(self): return { diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index 1feb1badfd1..2f6bdd4f81e 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -3,6 +3,7 @@ import difflib import logging import time +from markupsafe import escape from odoo import api, fields, models, Command, _ @@ -130,7 +131,7 @@ class AccountMove(models.Model): if not purchases: continue refs = [purchase._get_html_link() for purchase in purchases] - message = _("This vendor bill has been created from: %s") % ','.join(refs) + message = escape(_("This vendor bill has been created from: %s")) % ','.join(refs) move.message_post(body=message) return moves @@ -145,7 +146,7 @@ class AccountMove(models.Model): diff_purchases = new_purchases - old_purchases[i] if diff_purchases: refs = [purchase._get_html_link() for purchase in diff_purchases] - message = _("This vendor bill has been modified from: %s") % ','.join(refs) + message = escape(_("This vendor bill has been modified from: %s")) % ','.join(refs) move.message_post(body=message) return res diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 5281d3ec80b..7f1619c0f0b 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -3,7 +3,7 @@ from datetime import timedelta from itertools import groupby -from markupsafe import Markup +from markupsafe import escape from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.exceptions import AccessError, UserError, ValidationError @@ -989,10 +989,9 @@ class SaleOrder(models.Model): self.show_update_fpos = False if self.partner_id: - self.message_post(body=_( - "Product taxes have been recomputed according to fiscal position %s.", + self.message_post(body=escape(_("Product taxes have been recomputed according to fiscal position %s.")) % \ self.fiscal_position_id._get_html_link() if self.fiscal_position_id else "", - )) + ) def action_update_prices(self): self.ensure_one() @@ -1000,10 +999,8 @@ class SaleOrder(models.Model): self._recompute_prices() if self.pricelist_id: - message = _( - "Product prices have been recomputed according to pricelist %s.", - self.pricelist_id._get_html_link(), - ) + message = escape(_("Product prices have been recomputed according to pricelist %s.")) % \ + self.pricelist_id._get_html_link() else: message = _("Product prices have been recomputed.") self.message_post(body=message) @@ -1508,7 +1505,7 @@ class SaleOrder(models.Model): order.activity_schedule( 'sale.mail_act_sale_upsell', user_id=order.user_id.id or order.partner_id.user_id.id, - note=_("Upsell %(order)s for customer %(customer)s", order=order_ref, customer=customer_ref)) + note=escape(_("Upsell %(order)s for customer %(customer)s")) % {"order":order_ref, "customer":customer_ref}) def _prepare_analytic_account_data(self, prefix=None): """ Prepare SO analytic account creation values. diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index b5bb9c1c334..d8c3e0dacc8 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -3,6 +3,7 @@ from collections import defaultdict from datetime import timedelta +from markupsafe import Markup from odoo import api, fields, models, _ from odoo.exceptions import UserError @@ -1017,18 +1018,18 @@ class SaleOrderLine(models.Model): orders = self.mapped('order_id') for order in orders: order_lines = self.filtered(lambda x: x.order_id == order) - msg = "" + _("The ordered quantity has been updated.") + "
        " + msg = Markup("%s
          ") % _("The ordered quantity has been updated.") for line in order_lines: - msg += "
        • %s:
          " % line.product_id.display_name + msg += Markup("
        • %s:
          ") % line.product_id.display_name msg += _( "Ordered Quantity: %(old_qty)s -> %(new_qty)s", old_qty=line.product_uom_qty, new_qty=values["product_uom_qty"] - ) + "
          " + ) + Markup("
          ") if line.product_id.type in ('consu', 'product'): - msg += _("Delivered Quantity: %s", line.qty_delivered) + "
          " - msg += _("Invoiced Quantity: %s", line.qty_invoiced) + "
          " - msg += "
        " + msg += _("Delivered Quantity: %s", line.qty_delivered) + Markup("
        ") + msg += _("Invoiced Quantity: %s", line.qty_invoiced) + Markup("
        ") + msg += Markup("
      ") order.message_post(body=msg) def _check_line_unlink(self): diff --git a/addons/sale_project/models/sale_order_line.py b/addons/sale_project/models/sale_order_line.py index a4e1effddad..bbaac554216 100644 --- a/addons/sale_project/models/sale_order_line.py +++ b/addons/sale_project/models/sale_order_line.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict +from markupsafe import escape from odoo import api, Command, fields, models, _ from odoo.tools import format_amount @@ -67,7 +68,7 @@ class SaleOrderLine(models.Model): line.sudo()._timesheet_service_generation() # if the SO line created a task, post a message on the order if line.task_id and not has_task: - msg_body = _("Task Created (%s): %s", line.product_id.name, line.task_id._get_html_link()) + msg_body = escape(_("Task Created (%s): %s")) % (line.product_id.name, line.task_id._get_html_link()) line.order_id.message_post(body=msg_body) return lines @@ -179,7 +180,10 @@ class SaleOrderLine(models.Model): task = self.env['project.task'].sudo().create(values) self.write({'task_id': task.id}) # post message on task - task_msg = _("This task has been created from: %s (%s)", self.order_id._get_html_link(), self.product_id.name) + task_msg = escape(_("This task has been created from: %s (%s)")) % ( + self.order_id._get_html_link(), + self.product_id.name + ) task.message_post(body=task_msg) return task diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 01cff383010..62ffd9e54ff 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -4,6 +4,7 @@ from collections import defaultdict from datetime import timedelta +from markupsafe import escape from operator import itemgetter from odoo import _, api, Command, fields, models @@ -740,7 +741,7 @@ Please change the quantity done or the rounding precision of your unit of measur if not documents or not doc_orig: return - msg = _("The deadline has been automatically updated due to a delay on %s.", doc_orig[0]._get_html_link()) + msg = escape(_("The deadline has been automatically updated due to a delay on %s.") % doc_orig[0]._get_html_link()) msg_subject = _("Deadline updated due to delay on %s", doc_orig[0].name) # write the message on each document for doc in documents: diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 45a1e2562e4..ab7ca0f379f 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -6,6 +6,7 @@ import time from ast import literal_eval from datetime import date, timedelta from collections import defaultdict +from markupsafe import escape from odoo import SUPERUSER_ID, _, api, fields, models from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES @@ -1247,7 +1248,7 @@ class Picking(models.Model): 'backorder_id': picking.id }) picking.message_post( - body=_('The backorder %s has been created.', backorder_picking._get_html_link()) + body=escape(_('The backorder %s has been created.')) % backorder_picking._get_html_link() ) moves_to_backorder.write({'picking_id': backorder_picking.id}) moves_to_backorder.move_line_ids.package_level_id.write({'picking_id':backorder_picking.id}) diff --git a/addons/stock_delivery/models/stock_picking.py b/addons/stock_delivery/models/stock_picking.py index 8de91e5e37a..56fe85c1ffa 100644 --- a/addons/stock_delivery/models/stock_picking.py +++ b/addons/stock_delivery/models/stock_picking.py @@ -1,6 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict +from markupsafe import Markup import json from odoo import _, api, fields, models @@ -189,13 +190,13 @@ class StockPicking(models.Model): for p in previous_pickings - without_tracking: p.carrier_tracking_ref += "," + res['tracking_number'] order_currency = self.sale_id.currency_id or self.company_id.currency_id - msg = _( - "Shipment sent to carrier %(carrier_name)s for shipping with tracking number %(ref)s
      Cost: %(price).2f %(currency)s", - carrier_name=self.carrier_id.name, - ref=self.carrier_tracking_ref, - price=self.carrier_price, - currency=order_currency.name - ) + msg = _("Shipment sent to carrier %(carrier_name)s for shipping with tracking number %(ref)s", + carrier_name=self.carrier_id.name, + ref=self.carrier_tracking_ref) + \ + Markup("
      ") + \ + _("Cost: %(price).2f %(currency)s", + price=self.carrier_price, + currency=order_currency.name) self.message_post(body=msg) self._add_delivery_cost_to_so() @@ -234,9 +235,9 @@ class StockPicking(models.Model): except ValueError: carrier_trackers = self.carrier_tracking_url else: - msg = "Tracking links for shipment:
      " + msg = _("Tracking links for shipment:") + Markup("
      ") for tracker in carrier_trackers: - msg += '' + tracker[0] + '
      ' + msg += Markup('%s
      ') % (tracker[1], tracker[0]) self.message_post(body=msg) return self.env["ir.actions.actions"]._for_xml_id("delivery.act_delivery_trackers_url") diff --git a/addons/stock_picking_batch/models/stock_picking.py b/addons/stock_picking_batch/models/stock_picking.py index 8fe9885dff4..01d8d4f94c1 100644 --- a/addons/stock_picking_batch/models/stock_picking.py +++ b/addons/stock_picking_batch/models/stock_picking.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import escape + from odoo import _, api, Command, fields, models from odoo.osv import expression from odoo.exceptions import ValidationError @@ -245,7 +247,7 @@ class StockPicking(models.Model): pickings = self.filtered(lambda p: p.user_id.id != user_id) pickings.write({'user_id': user_id}) for pick in pickings: - log_message = _('Assigned to %s Responsible', (pick.batch_id._get_html_link())) + log_message = escape(_('Assigned to %s Responsible')) % pick.batch_id._get_html_link() pick.message_post(body=log_message) def action_view_batch(self): diff --git a/addons/stock_picking_batch/models/stock_picking_batch.py b/addons/stock_picking_batch/models/stock_picking_batch.py index 3adb8e6e39b..cb32a11e0f6 100644 --- a/addons/stock_picking_batch/models/stock_picking_batch.py +++ b/addons/stock_picking_batch/models/stock_picking_batch.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup + from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.osv.expression import AND @@ -239,7 +241,7 @@ class StockPickingBatch(models.Model): if has_no_qty_done(picking): empty_pickings.add(picking.id) picking.message_post( - body="%s: %s %s" % ( + body=Markup("%s: %s %s") % ( _("Transferred by"), _("Batch Transfer"), picking.batch_id.id, diff --git a/addons/test_mail/tests/test_mail_followers.py b/addons/test_mail/tests/test_mail_followers.py index dba59018ad2..4d52ba5eaa2 100644 --- a/addons/test_mail/tests/test_mail_followers.py +++ b/addons/test_mail/tests/test_mail_followers.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup + from odoo.addons.mail.tests.common import MailCommon from odoo.exceptions import AccessError from odoo.tests import tagged, users @@ -660,7 +662,7 @@ class RecipientsNotificationTest(MailCommon): 'status': 'sent', 'type': 'inbox'}], message_info={'content': 'User Choice Notification'}): test.message_post( - body='

      User Choice Notification

      ', + body=Markup('

      User Choice Notification

      '), message_type='comment', partner_ids=shared_partner.ids, subtype_xmlid='mail.mt_comment', diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py index c402c1da1a3..77d365e0341 100644 --- a/addons/test_mail/tests/test_mail_mail.py +++ b/addons/test_mail/tests/test_mail_mail.py @@ -7,6 +7,7 @@ import smtplib from datetime import datetime, timedelta from freezegun import freeze_time +from markupsafe import Markup from OpenSSL.SSL import Error as SSLError from socket import gaierror, timeout from unittest.mock import call, patch @@ -38,9 +39,9 @@ class TestMailMail(MailCommon): 'email_from': 'ignasse@example.com', }).with_context({}) - cls.test_message = cls.test_record.message_post(body='

      Message

      ', subject='Subject') + cls.test_message = cls.test_record.message_post(body=Markup('

      Message

      '), subject='Subject') cls.test_mail = cls.env['mail.mail'].create([{ - 'body': '

      Body

      ', + 'body': Markup('

      Body

      '), 'email_from': False, 'email_to': 'test@example.com', 'is_notification': True, diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py index d04e3aec4f6..d1609aa18af 100644 --- a/addons/test_mail/tests/test_mail_message.py +++ b/addons/test_mail/tests/test_mail_message.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 +from markupsafe import Markup from unittest.mock import patch from odoo.addons.mail.tests.common import mail_new_test_user, MailCommon @@ -483,7 +484,7 @@ class TestMessageAccess(MailCommon): test_record.message_subscribe((partner_1 | self.user_admin.partner_id).ids) message = test_record.message_post( - body='

      This is First Message

      ', subject='Subject', + body=Markup('

      This is First Message

      '), subject='Subject', message_type='comment', subtype_xmlid='mail.mt_note') # portal user have no rights to read the message with self.assertRaises(AccessError): diff --git a/addons/test_mail/tests/test_mail_thread_internals.py b/addons/test_mail/tests/test_mail_thread_internals.py index 4bafa0458c5..bcee37e22ea 100644 --- a/addons/test_mail/tests/test_mail_thread_internals.py +++ b/addons/test_mail/tests/test_mail_thread_internals.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup from unittest.mock import patch from unittest.mock import DEFAULT @@ -36,7 +37,7 @@ class TestAPI(MailCommon, TestRecipients): # post a note message = ticket_record.message_post( attachment_ids=attachments.ids, - body="

      Initial Body

      ", + body=Markup("

      Initial Body

      "), message_type="comment", partner_ids=self.partner_1.ids, ) @@ -543,7 +544,7 @@ class TestNoThread(MailCommon, TestRecipients): 'subtype': 'mail.mt_note', }]): _message = self.env['mail.thread'].message_notify( - body='

      Hello Paulo

      ', + body=Markup('

      Hello Paulo

      '), partner_ids=self.partner_2.ids, subject='Test Notify', ) diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index e5471931dde..2d0ead3f7f3 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -6,7 +6,7 @@ import base64 from datetime import datetime, timedelta from freezegun import freeze_time from itertools import product -from markupsafe import escape +from markupsafe import escape, Markup from unittest.mock import patch from odoo import tools @@ -295,7 +295,7 @@ class TestMessageNotify(TestMessagePostCommon): with self.mock_mail_gateway(): new_notification = test_record.message_notify( - body='

      You have received a notification

      ', + body=Markup('

      You have received a notification

      '), partner_ids=[self.partner_1.id, self.partner_admin.id, self.partner_employee_2.id], subject='This should be a subject', ) @@ -342,7 +342,7 @@ class TestMessageNotify(TestMessagePostCommon): with self.mock_mail_gateway(): new_notification = test_record.message_notify( - body='

      You have received a notification

      ', + body=Markup('

      You have received a notification

      '), partner_ids=(self.partner_1 + self.partner_employee).ids, subject='This should be a subject', ) @@ -351,7 +351,7 @@ class TestMessageNotify(TestMessagePostCommon): with self.mock_mail_gateway(): new_notification = test_record.message_notify( - body='

      You have received a notification

      ', + body=Markup('

      You have received a notification

      '), notify_author=True, partner_ids=(self.partner_1 + self.partner_employee).ids, subject='This should be a subject', @@ -365,7 +365,7 @@ class TestMessageNotify(TestMessagePostCommon): with self.mock_mail_gateway(): new_notification = test_record.with_context(mail_notify_author=True).message_notify( - body='

      You have received a notification

      ', + body=Markup('

      You have received a notification

      '), partner_ids=(self.partner_1 + self.partner_employee).ids, subject='This should be a subject', ) @@ -383,7 +383,7 @@ class TestMessageNotify(TestMessagePostCommon): with self.assertRaises(ValueError): test_records.message_notify( - body='

      Nice notification content

      ', + body=Markup('

      Nice notification content

      '), partner_ids=self.partner_employee_2.ids, subject='Notify Subject', ) @@ -473,7 +473,7 @@ class TestMessageNotify(TestMessagePostCommon): self.mock_mail_gateway(), \ self.assertRaises(ValueError): _new_message = test_record.message_notify( - body='

      You will not receive a notification

      ', + body=Markup('

      You will not receive a notification

      '), partner_ids=self.partner_1.ids, subject='This should not be accepted', **parameters @@ -481,7 +481,7 @@ class TestMessageNotify(TestMessagePostCommon): # support of subtype xml id new_message = test_record.message_notify( - body='

      You will not receive a notification

      ', + body=Markup('

      You will not receive a notification

      '), partner_ids=self.partner_1.ids, subtype_xmlid='mail.mt_note', ) @@ -494,7 +494,7 @@ class TestMessageNotify(TestMessagePostCommon): people without having a document. """ with self.mock_mail_gateway(): new_notification = self.env['mail.thread'].message_notify( - body='

      You have received a notification

      ', + body=Markup('

      You have received a notification

      '), partner_ids=[self.partner_1.id, self.partner_admin.id, self.partner_employee_2.id], subject='This should be a subject', ) @@ -535,7 +535,7 @@ class TestMessageLog(TestMessagePostCommon): test_record.message_subscribe(self.partner_employee_2.ids) new_note = test_record._message_log( - body='

      Labrador

      ', + body=Markup('

      Labrador

      '), ) self.assertMessageFields( new_note, @@ -559,7 +559,7 @@ class TestMessageLog(TestMessagePostCommon): new_notes = test_records._message_log_batch( bodies=dict( - (test_record.id, '

      Test _message_log_batch

      ') + (test_record.id, Markup('

      Test _message_log_batch

      ')) for test_record in test_records ), ) @@ -833,7 +833,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): self.assertMsgWithoutNotifications(), \ self.capture_triggers(cron_id) as capt: msg = test_record.message_post( - body='

      Test

      ', + body=Markup('

      Test

      '), message_type='comment', subject='Subject', subtype_xmlid='mail.mt_comment', @@ -880,7 +880,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): self.mock_mail_gateway(mail_unlink_sent=False), \ self.capture_triggers(cron_id) as capt: msg = test_record.message_post( - body='

      Test

      ', + body=Markup('

      Test

      '), message_type='comment', subject='Subject', subtype_xmlid='mail.mt_comment', @@ -908,7 +908,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): with freeze_time(now), \ self.assertMsgWithoutNotifications(): msg = test_record.message_post( - body='

      Test

      ', + body=Markup('

      Test

      '), message_type='comment', subject='Subject', subtype_xmlid='mail.mt_comment', @@ -1086,7 +1086,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): ] ), patch.object(MailTestSimple, 'check_access_rights', return_value=True): new_msg = self.test_record.with_user(self.user_portal).message_post( - body='

      Test

      ', + body=Markup('

      Test

      '), message_type='comment', subject='Subject', subtype_xmlid='mail.mt_comment', @@ -1095,7 +1095,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): with self.assertRaises(AccessError): self.test_record.with_user(self.user_portal).message_post( - body='

      Test

      ', + body=Markup('

      Test

      '), message_type='comment', subject='Subject', subtype_xmlid='mail.mt_comment', @@ -1108,7 +1108,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): with self.mock_mail_gateway(): parent_msg = test_record.message_post( - body='

      Test

      ', + body=Markup('

      Test

      '), message_type='comment', subject='Test Subject', subtype_xmlid='mail.mt_comment', @@ -1121,7 +1121,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): [{'content': '

      Test Answer

      ', 'notif': [{'partner': self.partner_1, 'type': 'email'}]}] ): msg = test_record.message_post( - body='

      Test Answer

      ', + body=Markup('

      Test Answer

      '), message_type='comment', parent_id=parent_msg.id, partner_ids=[self.partner_1.id], @@ -1144,7 +1144,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): # post a reply to the reply: check parent is the first one with self.mock_mail_gateway(): new_msg = test_record.message_post( - body='

      Test Answer Bis

      ', + body=Markup('

      Test Answer Bis

      '), message_type='comment', subtype_xmlid='mail.mt_comment', parent_id=msg.id, @@ -1697,7 +1697,7 @@ class TestMessagePostLang(MailCommon, TestRecipients): with self.mock_mail_gateway(): test_records[1].message_post( - body='

      Hello

      ', + body=Markup('

      Hello

      '), email_layout_xmlid='mail.test_layout', message_type='comment', subject='Subject', @@ -1820,7 +1820,7 @@ class TestMessagePostLang(MailCommon, TestRecipients): with self.mock_mail_gateway(mail_unlink_sent=False), \ self.mock_mail_app(): record.message_post( - body='

      Hi there

      ', + body=Markup('

      Hi there

      '), email_layout_xmlid=email_layout_xmlid, message_type='comment', subject='TeDeum', diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index ce57c95d01e..9f8508f6d57 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from contextlib import nullcontext +from markupsafe import Markup from unittest.mock import patch from odoo.addons.base.tests.common import TransactionCaseWithUserDemo @@ -589,7 +589,7 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(admin=1, employee=1): record._message_log( - body='

      Test _message_log

      ', + body=Markup('

      Test _message_log

      '), message_type='comment') @users('admin', 'employee') @@ -603,7 +603,7 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(admin=1, employee=1): records._message_log_batch( bodies=dict( - (record.id, '

      Test _message_log

      ') + (record.id, Markup('

      Test _message_log

      ')) for record in records ), message_type='comment') @@ -629,7 +629,7 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(admin=7, employee=7): record.message_post( - body='

      Test message_post as log

      ', + body=Markup('

      Test message_post as log

      '), subtype_xmlid='mail.mt_note', message_type='comment') @@ -640,7 +640,7 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(admin=7, employee=7): record.message_post( - body='

      Test Post Performances basic

      ', + body=Markup('

      Test Post Performances basic

      '), partner_ids=[], message_type='comment', subtype_xmlid='mail.mt_comment') @@ -653,7 +653,7 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(admin=30, employee=30): record.message_post( - body='

      Test Post Performances with an email ping

      ', + body=Markup('

      Test Post Performances with an email ping

      '), partner_ids=self.customer.ids, message_type='comment', subtype_xmlid='mail.mt_comment') @@ -665,7 +665,7 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(admin=17, employee=17): record.message_post( - body='

      Test Post Performances with an inbox ping

      ', + body=Markup('

      Test Post Performances with an inbox ping

      '), partner_ids=self.user_test.partner_id.ids, message_type='comment', subtype_xmlid='mail.mt_comment') @@ -854,7 +854,7 @@ class TestMailComplexPerformance(BaseMailPerformance): # about 20 (19?) queries per additional customer group with self.assertQueryCount(admin=53, employee=52): record.message_post( - body='

      Test Post Performances

      ', + body=Markup('

      Test Post Performances

      '), message_type='comment', subtype_xmlid='mail.mt_comment') @@ -1311,7 +1311,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): # with self.assertQueryCount(employee=63), enable_logging: with self.assertQueryCount(employee=60): record_container.with_context({}).message_post( - body='

      Test body

      ', + body=Markup('

      Test body

      '), subject='Test Subject', message_type='notification', subtype_xmlid=None, diff --git a/addons/test_mail_full/tests/test_mail_performance.py b/addons/test_mail_full/tests/test_mail_performance.py index 5cd06c4de67..6a7a049c689 100644 --- a/addons/test_mail_full/tests/test_mail_performance.py +++ b/addons/test_mail_full/tests/test_mail_performance.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup from odoo.addons.mail.tests.common import mail_new_test_user from odoo.addons.test_mail.tests.test_performance import BaseMailPerformance from odoo.tests.common import users, warmup @@ -84,7 +85,7 @@ class TestMailPerformance(BaseMailPerformance): with self.assertQueryCount(employee=85): # tmf: 85 new_message = record_ticket.message_post( attachment_ids=attachments.ids, - body='

      Test Content

      ', + body=Markup('

      Test Content

      '), message_type='comment', subject='Test Subject', subtype_xmlid='mail.mt_comment', diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py index 30f65fe277d..51200f659dc 100644 --- a/addons/website_crm_partner_assign/models/crm_lead.py +++ b/addons/website_crm_partner_assign/models/crm_lead.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import random +from markupsafe import Markup from odoo import api, fields, models, _ from odoo.exceptions import AccessDenied, AccessError, UserError @@ -189,23 +190,23 @@ class CrmLead(models.Model): return res_partner_ids def partner_interested(self, comment=False): - message = _('

      I am interested by this lead.

      ') + message = Markup('

      %s

      ') % _('I am interested by this lead.') if comment: - message += '

      %s

      ' % html_escape(comment) + message += Markup('

      %s

      ') % comment for lead in self: lead.message_post(body=message) lead.sudo().convert_opportunity(lead.partner_id) # sudo required to convert partner data def partner_desinterested(self, comment=False, contacted=False, spam=False): if contacted: - message = '

      %s

      ' % _('I am not interested by this lead. I contacted the lead.') + message = Markup('

      %s

      ') % _('I am not interested by this lead. I contacted the lead.') else: - message = '

      %s

      ' % _('I am not interested by this lead. I have not contacted the lead.') + message = Markup('

      %s

      ') % _('I am not interested by this lead. I have not contacted the lead.') partner_ids = self.env['res.partner'].search( [('id', 'child_of', self.env.user.partner_id.commercial_partner_id.id)]) self.message_unsubscribe(partner_ids=partner_ids.ids) if comment: - message += '

      %s

      ' % html_escape(comment) + message += Markup('

      %s

      ') % comment self.message_post(body=message) values = { 'partner_assigned_id': False, diff --git a/addons/website_payment/models/payment_transaction.py b/addons/website_payment/models/payment_transaction.py index e416179de3e..d8d9aefb48a 100644 --- a/addons/website_payment/models/payment_transaction.py +++ b/addons/website_payment/models/payment_transaction.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup from odoo import _, fields, models @@ -20,8 +21,8 @@ class PaymentTransaction(models.Model): if value: if hasattr(value, 'name'): value = value.name - msg.append('
      - %s: %s' % (field_name, value)) - tx.payment_id._message_log(body=''.join(msg)) + msg.append(Markup('
      - %s: %s') % (field_name, value)) + tx.payment_id._message_log(body=Markup().join(msg)) def _send_donation_email(self, is_internal_notification=False, comment=None, recipient_email=None): self.ensure_one()