# Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import json from markupsafe import Markup from psycopg2 import IntegrityError import re from werkzeug.exceptions import BadRequest from odoo import http, SUPERUSER_ID 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 AccessDenied, ValidationError, UserError from odoo.tools.misc import hmac, consteq from odoo.tools.translate import _, LazyTranslate _lt = LazyTranslate(__name__) class WebsiteForm(http.Controller): @http.route('/website/form', type='http', auth="public", methods=['POST'], multilang=False, readonly=True) def website_form_empty(self, **kwargs): # This is a workaround to don't add language prefix to
return "" # Check and insert values from the form on the model @http.route('/website/form/', type='http', auth="public", methods=['POST'], website=True, csrf=False) def website_form(self, model_name, **kwargs): # Partial CSRF check, only performed when session is authenticated, as there # is no real risk for unauthenticated sessions here. It's a common case for # embedded forms now: SameSite policy rejects the cookies, so the session # is lost, and the CSRF check fails, breaking the post for no good reason. csrf_token = request.params.pop('csrf_token', None) if request.session.uid and not request.validate_csrf(csrf_token): raise BadRequest('Session expired (invalid CSRF token)') try: # The except clause below should not let what has been done inside # here be committed. It should not either roll back everything in # this controller method. Instead, we use a savepoint to roll back # what has been done inside the try clause. with request.env.cr.savepoint(): if request.env['ir.http']._verify_request_recaptcha_token('website_form'): # request.params was modified, update kwargs to reflect the changes kwargs = dict(request.params) kwargs.pop('model_name') return self._handle_website_form(model_name, **kwargs) error = _("Suspicious activity detected by Google reCaptcha.") except (ValidationError, UserError) as e: error = e.args[0] return json.dumps({ 'error': error, }) def _handle_website_form(self, model_name, **kwargs): model_record = request.env['ir.model'].sudo().search([('model', '=', model_name), ('website_form_access', '=', True)]) if not model_record: return json.dumps({ 'error': _("The form's specified model does not exist") }) try: data = self.extract_data(model_record, kwargs) # If we encounter an issue while extracting data except ValidationError as e: # I couldn't find a cleaner way to pass data to an exception return json.dumps({'error_fields': e.args[0]}) try: id_record = self.insert_record(request, model_record, data['record'], data['custom'], data.get('meta')) if id_record: self.insert_attachment(model_record, id_record, data['attachments']) # in case of an email, we want to send it immediately instead of waiting # 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 # Ex: crm.lead.probability which is a float between 0 and 1 # TODO: How to get the name of the erroneous field ? except IntegrityError: return json.dumps(False) request.session['form_builder_model_model'] = model_record.model request.session['form_builder_model'] = model_record.name request.session['form_builder_id'] = id_record return json.dumps({'id': id_record}) # Constants string to make metadata readable on a text field _meta_label = _lt("Metadata") # Title for meta data # Dict of dynamically called filters following type of field to be fault tolerent def identity(self, field_label, field_input): return field_input def integer(self, field_label, field_input): return int(field_input) def floating(self, field_label, field_input): return float(field_input) def html(self, field_label, field_input): return plaintext2html(field_input) def boolean(self, field_label, field_input): return bool(field_input) def binary(self, field_label, field_input): return base64.b64encode(field_input.read()) def one2many(self, field_label, field_input): return [int(i) for i in field_input.split(',')] def many2many(self, field_label, field_input, *args): return [(args[0] if args else (6, 0)) + (self.one2many(field_label, field_input),)] def tags(self, field_label, field_input): # Unescape ',' and '\' return [ tag.replace('\\,', ',').replace('\\/', '\\') for tag in re.split(r'(?', company=request.env.company.name, email=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) authenticate_message = False email_field_name = request.env[model_name]._mail_get_primary_email_field() if email_field_name and hasattr(record, '_message_log') and email_field_name in values: warning_icon = "" if request.session.uid: user_email = request.env.user.email form_email = values[email_field_name] if user_email != form_email: authenticate_message = _("This %(model_name)s was submitted by %(user_name)s (%(user_email)s) on behalf of %(form_email)s", model_name=model.name, user_name=request.env.user.name, user_email=user_email, form_email=form_email) elif self._should_log_authenticate_message(record): warning_icon = "/!\\ " authenticate_message = _("EXTERNAL SUBMISSION - Customer not verified") if authenticate_message: record._message_log( body=Markup('').format(warning_icon=warning_icon, message=authenticate_message), ) if custom or meta or authenticate_message: _custom_label = "%s\n___________\n\n" % _("Other Information:") # Title for custom fields if model_name == 'mail.mail': _custom_label = "%s\n___________\n\n" % _("This message has been posted on your website!") default_field = model.website_form_default_field_id default_field_data = values.get(default_field.name, '') custom_label = _custom_label + custom if custom else '' meta_label = self._meta_label + "\n________\n\n" + meta if meta else '' custom_content = '' for text in [authenticate_message, default_field_data, custom_label, meta_label]: if not text: continue if custom_content: custom_content += '\n\n' custom_content += text # If there is a default field configured for this model, use it. # If there isn't, put the custom data in a message instead if default_field.name: if default_field.ttype == 'html' or model_name == 'mail.mail': custom_content = nl2br_enclose(custom_content) record.update({default_field.name: custom_content}) elif hasattr(record, '_message_log'): record._message_log( body=nl2br_enclose(custom_content, 'p'), message_type='comment', ) return record.id # Link all files attached on the form def insert_attachment(self, model, id_record, files): orphan_attachment_ids = [] model_name = model.sudo().model record = model.env[model_name].browse(id_record) authorized_fields = model.with_user(SUPERUSER_ID)._get_form_writable_fields() for file in files: custom_field = file.field_name not in authorized_fields attachment_value = { 'name': file.filename, 'datas': base64.encodebytes(file.read()), 'res_model': model_name, 'res_id': record.id, } attachment_id = request.env['ir.attachment'].sudo().create(attachment_value) if attachment_id and not custom_field: record_sudo = record.sudo() value = [(4, attachment_id.id)] if record_sudo._fields[file.field_name].type == 'many2one': value = attachment_id.id record_sudo[file.field_name] = value else: orphan_attachment_ids.append(attachment_id.id) if model_name != 'mail.mail' and hasattr(record, '_message_log') and orphan_attachment_ids: # If some attachments didn't match a field on the model, # we create a mail.message to link them to the record record._message_log( attachment_ids=[(6, 0, orphan_attachment_ids)], body=Markup(_('

Attached files:

')), message_type='comment', ) elif model_name == 'mail.mail' and orphan_attachment_ids: # If the model is mail.mail then we have no other choice but to # attach the custom binary field files on the attachment_ids field. for attachment_id_id in orphan_attachment_ids: record.attachment_ids = [(4, attachment_id_id)]