[FIX] website: make forms work again when inside html fields

Recent commit [1] in Odoo 15 (which is a backport of commit [2] which
was merged in Odoo 17) introduced a security layer on forms but only
for forms which are inside `ir.ui.view`. The forms inside HTML fields
are thus not working anymore, because those don't receive the required
signature.

For the record:
- `ir.ui.view` = `website.page` pages, some part of the controller pages
- HTML fields = the most part of the editable areas in controller pages

[1]: https://github.com/odoo/odoo/commit/17c6f6f30bf13bd3c303b28d9a314bd76dd8f4dc
[2]: https://github.com/odoo/odoo/commit/7d25e7bf9243367c87b1b2005587ae728730b49c

opw-3586333

closes odoo/odoo#143139

closes odoo/odoo#143879

X-original-commit: 5e5a14ae4a522a589f6112d1687bb910f86f9a6d
Signed-off-by: Jérémy Kersten <jke@odoo.com>
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
Co-authored-by: Vranckx Florian (flvr) <flvr@odoo.com>
Co-authored-by: Romain Derie <rde@odoo.com>
This commit is contained in:
Romain Derie
2023-11-30 07:37:14 +00:00
co-authored by Vranckx Florian
parent 731811e409
commit 7c6844fa6c
6 changed files with 79 additions and 12 deletions
+14 -3
View File
@@ -12,7 +12,8 @@ from odoo import http, SUPERUSER_ID, _, _lt
from odoo.addons.base.models.ir_qweb_fields import nl2br, nl2br_enclose
from odoo.http import request
from odoo.tools import plaintext2html
from odoo.exceptions import ValidationError, UserError
from odoo.exceptions import AccessDenied, ValidationError, UserError
from odoo.tools.misc import hmac, consteq
class WebsiteForm(http.Controller):
@@ -73,6 +74,15 @@ class WebsiteForm(http.Controller):
# for the email queue to process
if model_name == 'mail.mail':
form_has_email_cc = {'email_cc', 'email_bcc'} & kwargs.keys() or \
'email_cc' in kwargs["website_form_signature"]
# remove the email_cc information from the signature
kwargs["website_form_signature"] = kwargs["website_form_signature"].split(':')[0]
if kwargs.get("email_to"):
value = kwargs['email_to'] + (':email_cc' if form_has_email_cc else '')
hash_value = hmac(model_record.env, 'website_form_signature', value)
if not consteq(kwargs["website_form_signature"], hash_value):
raise AccessDenied('invalid website_form_signature')
request.env[model_name].sudo().browse(id_record).send()
# Some fields have additional SQL constraints that we can't check generically
@@ -184,7 +194,7 @@ class WebsiteForm(http.Controller):
custom_fields.append((_('email'), field_value))
# If it's a custom field
elif field_name != 'context':
elif field_name not in ('context', 'website_form_signature'):
custom_fields.append((field_name, field_value))
data['custom'] = "\n".join([u"%s : %s" % v for v in custom_fields])
@@ -217,7 +227,8 @@ class WebsiteForm(http.Controller):
def insert_record(self, request, model, values, custom, meta=None):
model_name = model.sudo().model
if model_name == 'mail.mail':
values.update({'reply_to': values.get('email_from')})
email_from = _('"%s form submission" <%s>') % (request.env.company.name, request.env.company.email)
values.update({'reply_to': values.get('email_from'), 'email_from': email_from})
record = request.env[model_name].with_user(SUPERUSER_ID).with_context(
mail_create_nosubscribe=True,
).create(values)
+21
View File
@@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import etree
from markupsafe import Markup
from odoo import api, models, _
from odoo.addons.website.tools import add_form_signature
class Contact(models.AbstractModel):
@@ -16,3 +20,20 @@ class Contact(models.AbstractModel):
badges=dict(type='boolean', string=_('Display the badges'))
)
return options
class HTML(models.AbstractModel):
_inherit = 'ir.qweb.field.html'
@api.model
def value_to_html(self, value, options):
res = super().value_to_html(value, options)
if res and '<form' in res: # Efficient check
# The usage of `fromstring`, `HTMLParser`, `tostring` and `Markup`
# is replicating what is done in the `super()` implementation.
body = etree.fromstring("<body>%s</body>" % res, etree.HTMLParser())[0]
add_form_signature(body, self.sudo().env)
res = Markup(etree.tostring(body, encoding='unicode', method='html')[6:-7])
return res
+6
View File
@@ -7,6 +7,7 @@ import werkzeug
from odoo import api, fields, models
from odoo import tools
from odoo.addons.website.tools import add_form_signature
from odoo.exceptions import AccessError
from odoo.osv import expression
from odoo.http import request
@@ -493,6 +494,11 @@ class View(models.Model):
'data-bg-video-src', 'data-shape', 'data-scroll-background-ratio',
]
def _get_combined_arch(self):
root = super()._get_combined_arch()
add_form_signature(root, self.sudo().env)
return root
# --------------------------------------------------------------------------
# Snippet saving
# --------------------------------------------------------------------------
+10 -7
View File
@@ -31,13 +31,16 @@ class website_form_model(models.Model):
builders and are writable. By default no field is writable by the
form builder.
"""
included = {
field.name
for field in self.env['ir.model.fields'].sudo().search([
('model_id', '=', self.id),
('website_form_blacklisted', '=', False)
])
}
if self.model == "mail.mail":
included = {'email_from', 'email_to', 'email_cc', 'email_bcc', 'body', 'reply_to', 'subject'}
else:
included = {
field.name
for field in self.env['ir.model.fields'].sudo().search([
('model_id', '=', self.id),
('website_form_blacklisted', '=', False)
])
}
return {
k: v for k, v in self.get_authorized_fields(self.model).items()
if k in included
@@ -28,7 +28,8 @@ registry.category("web_tour.tours").add("website_form_editor_tour_submit", {
":has(.s_website_form_field.s_website_form_required:has(label:contains('State')):has(select[name='State'][required]:has(option[value='France'])))" +
":has(.s_website_form_field:has(label:contains('State')):has(select[name='State'][required]:has(option[value='Canada'])))" +
":has(.s_website_form_field:has(label:contains('Invoice Scan')))" +
":has(.s_website_form_field:has(input[name='email_to'][value='test@test.test']))",
":has(.s_website_form_field:has(input[name='email_to'][value='test@test.test']))" +
":has(.s_website_form_field:has(input[name='website_form_signature']))",
trigger: ".s_website_form_send"
},
{
+26 -1
View File
@@ -10,7 +10,7 @@ from werkzeug.test import EnvironBuilder
import odoo
from odoo.tests.common import HttpCase, HOST
from odoo.tools.misc import DotDict, frozendict
from odoo.tools.misc import hmac, DotDict, frozendict
@contextlib.contextmanager
@@ -187,3 +187,28 @@ def get_base_domain(url, strip_www=False):
if strip_www and url.startswith('www.'):
url = url[4:]
return url
def add_form_signature(html_fragment, env_sudo):
for form in html_fragment.iter('form'):
if '/website/form/' not in form.attrib.get('action', ''):
continue
existing_hash_node = form.find('.//input[@type="hidden"][@name="website_form_signature"]')
if existing_hash_node is not None:
existing_hash_node.getparent().remove(existing_hash_node)
input_nodes = form.xpath('.//input[contains(@name, "email_")]')
form_values = {input_node.attrib['name']: input_node for input_node in input_nodes}
# if this form does not send an email, ignore. But at this stage,
# the value of email_to can still be None in case of default value
if 'email_to' not in form_values:
continue
elif not form_values['email_to'].attrib.get('value'):
form_values['email_to'].attrib['value'] = env_sudo.company.email or ''
has_cc = {'email_cc', 'email_bcc'} & form_values.keys()
value = form_values['email_to'].attrib['value'] + (':email_cc' if has_cc else '')
hash_value = hmac(env_sudo, 'website_form_signature', value)
if has_cc:
hash_value += ':email_cc'
hash_node = etree.Element('input', attrib={'type': "hidden", 'value': hash_value, 'class': "form-control s_website_form_input s_website_form_custom", 'name': "website_form_signature"})
form_values['email_to'].addnext(hash_node)