diff options
Diffstat (limited to 'addons/website_form/controllers/main.py')
| -rw-r--r-- | addons/website_form/controllers/main.py | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py new file mode 100644 index 00000000..1d4daba4 --- /dev/null +++ b/addons/website_form/controllers/main.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import json +import pytz + +from datetime import datetime +from psycopg2 import IntegrityError +from werkzeug.exceptions import BadRequest + +from odoo import http, SUPERUSER_ID, _ +from odoo.http import request +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools.translate import _ +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 <form action="/website_form/" ...> + return "" + + # Check and insert values from the form on the model <model> + @http.route('/website_form/<string:model_name>', 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 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': identity, + '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', + 'no_auto_thread': False, + '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': _('<p>Attached files : </p>'), + 'model': model_name, + 'message_type': 'comment', + 'no_auto_thread': False, + '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)] |
