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'])
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 = "
") % (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"
+ body=Markup("%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.
') % _("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 += "
'),
'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="
'),
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='
'),
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='
'),
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()