# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import json from psycopg2 import IntegrityError from werkzeug.exceptions import BadRequest from odoo import http, SUPERUSER_ID, _ from odoo.http import request from odoo.tools import plaintext2html from odoo.exceptions import ValidationError, UserError from odoo.addons.base.models.ir_qweb_fields import nl2br class WebsiteForm(http.Controller): @http.route('/website/form/', type='http', auth="public", methods=['POST'], multilang=False) 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'): 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, request.params) # 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': 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 = "%s\n________\n\n" % _("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),)] _input_filters = { 'char': identity, 'text': identity, 'html': html, 'date': identity, 'datetime': identity, 'many2one': integer, 'one2many': one2many, 'many2many':many2many, 'selection': identity, 'boolean': boolean, 'integer': integer, 'float': floating, 'binary': binary, 'monetary': floating, } # Extract all data sent by the form and sort its on several properties def extract_data(self, model, values): dest_model = request.env[model.sudo().model] data = { 'record': {}, # Values to create record 'attachments': [], # Attached files 'custom': '', # Custom fields values 'meta': '', # Add metadata if enabled } authorized_fields = model.sudo()._get_form_writable_fields() error_fields = [] custom_fields = [] for field_name, field_value in values.items(): # If the value of the field if a file if hasattr(field_value, 'filename'): # Undo file upload field name indexing field_name = field_name.split('[', 1)[0] # If it's an actual binary field, convert the input file # If it's not, we'll use attachments instead if field_name in authorized_fields and authorized_fields[field_name]['type'] == 'binary': data['record'][field_name] = base64.b64encode(field_value.read()) field_value.stream.seek(0) # do not consume value forever if authorized_fields[field_name]['manual'] and field_name + "_filename" in dest_model: data['record'][field_name + "_filename"] = field_value.filename else: field_value.field_name = field_name data['attachments'].append(field_value) # If it's a known field elif field_name in authorized_fields: try: input_filter = self._input_filters[authorized_fields[field_name]['type']] data['record'][field_name] = input_filter(self, field_name, field_value) except ValueError: error_fields.append(field_name) # If it's a custom field elif field_name != 'context': custom_fields.append((field_name, field_value)) data['custom'] = "\n".join([u"%s : %s" % v for v in custom_fields]) # Add metadata if enabled # ICP for retrocompatibility if request.env['ir.config_parameter'].sudo().get_param('website_form_enable_metadata'): environ = request.httprequest.headers.environ data['meta'] += "%s : %s\n%s : %s\n%s : %s\n%s : %s\n" % ( "IP" , environ.get("REMOTE_ADDR"), "USER_AGENT" , environ.get("HTTP_USER_AGENT"), "ACCEPT_LANGUAGE" , environ.get("HTTP_ACCEPT_LANGUAGE"), "REFERER" , environ.get("HTTP_REFERER") ) # This function can be defined on any model to provide # a model-specific filtering of the record values # Example: # def website_form_input_filter(self, values): # values['name'] = '%s\'s Application' % values['partner_name'] # return values if hasattr(dest_model, "website_form_input_filter"): data['record'] = dest_model.website_form_input_filter(request, data['record']) missing_required_fields = [label for label, field in authorized_fields.items() if field['required'] and not label in data['record']] if any(error_fields): raise ValidationError(error_fields + missing_required_fields) return data 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')}) record = request.env[model_name].with_user(SUPERUSER_ID).with_context(mail_create_nosubscribe=True).create(values) if custom or meta: _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_content = (default_field_data + "\n\n" if default_field_data else '') \ + (_custom_label + custom + "\n\n" if custom else '') \ + (self._meta_label + meta if meta else '') # 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(custom_content) record.update({default_field.name: custom_content}) else: values = { 'body': nl2br(custom_content), 'model': model_name, 'message_type': 'comment', 'res_id': record.id, } mail_id = request.env['mail.message'].with_user(SUPERUSER_ID).create(values) 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.sudo()._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()[file.field_name] = [(4, attachment_id.id)] else: orphan_attachment_ids.append(attachment_id.id) if model_name != 'mail.mail': # If some attachments didn't match a field on the model, # we create a mail.message to link them to the record if orphan_attachment_ids: values = { 'body': _('

Attached files :

'), 'model': model_name, 'message_type': 'comment', 'res_id': id_record, 'attachment_ids': [(6, 0, orphan_attachment_ids)], 'subtype_id': request.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'), } mail_id = request.env['mail.message'].with_user(SUPERUSER_ID).create(values) else: # 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)]