Chrome recently changed their SameSite policy default value from None to Lax, the session is no more shared between the webpage and the iframe. As a result, the csrf check systematically fails. After this commit, the csrf_token check is only made when you have a session. In case you are using your form in an iframe on another site, with the new cookies policy, your cookies with the session_id (linked to the csrf token) is not sent to the server and the check csrf always fails. Since the purpose of the csrf is to prevent another website to submit a form with your 'authenticated account', we can consider that if you are not logged and so have no session_id, it is no critical and we can ignore the csrf check. opw-2330286 closes odoo/odoo#58050 X-original-commit: 9a0c9f3192bc7043add89446cbe6c2650499a654 Signed-off-by: Olivier Dony (odo) <odo@openerp.com> Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
266 lines
12 KiB
Python
266 lines
12 KiB
Python
# -*- 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):
|
|
|
|
# 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:
|
|
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)],
|
|
}
|
|
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)]
|