diff --git a/addons/website/controllers/form.py b/addons/website/controllers/form.py index 56fee82ef72..a2d1a00212b 100644 --- a/addons/website/controllers/form.py +++ b/addons/website/controllers/form.py @@ -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) diff --git a/addons/website/models/ir_qweb_fields.py b/addons/website/models/ir_qweb_fields.py index 9f2a98a4e7f..d9c879210bc 100644 --- a/addons/website/models/ir_qweb_fields.py +++ b/addons/website/models/ir_qweb_fields.py @@ -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 '%s" % res, etree.HTMLParser())[0] + add_form_signature(body, self.sudo().env) + res = Markup(etree.tostring(body, encoding='unicode', method='html')[6:-7]) + + return res diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index fa63543345e..e16290e73cb 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -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 # -------------------------------------------------------------------------- diff --git a/addons/website/models/website_form.py b/addons/website/models/website_form.py index 3d9f32ffcef..896568595ef 100644 --- a/addons/website/models/website_form.py +++ b/addons/website/models/website_form.py @@ -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 diff --git a/addons/website/static/tests/tours/website_form_editor_frontend.js b/addons/website/static/tests/tours/website_form_editor_frontend.js index 96fa22cd1f5..cd2ebb28083 100644 --- a/addons/website/static/tests/tours/website_form_editor_frontend.js +++ b/addons/website/static/tests/tours/website_form_editor_frontend.js @@ -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" }, { diff --git a/addons/website/tools.py b/addons/website/tools.py index 01145f47c0f..952ab0d5578 100644 --- a/addons/website/tools.py +++ b/addons/website/tools.py @@ -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)