When submitting a ticket, if the user adds an attachement, he will not be able to see it on the ticket web page. To reproduce the error: (Need helpdesk,contacts) 1. Go to the settings of a Helpdesk Team (e.g., "Customer Care") 2. Enable "Website Form", Refresh 3. Go on Helpdesk web page and Publish the form 4. Create a partner P and grant him the portal access 5. Sign in with P 6. Submit a ticket with one attachment 7. Consult the ticket's web page Error: in "Message and communication history", OdooBot's message with the attachment is not displayed. However, when consulting the same page with the admin account, this message is considered as "Published". Thus, P should see the message and the attachment. When getting the messages, if the user is not an employee, the domain is restricted: https://github.com/odoo/odoo/blob/e92ac60a75462dfd830254a3d0dd5d7effe1528e/addons/portal/controllers/mail.py#L128-L130 with https://github.com/odoo/odoo/blob/cc32b7d1b396a42dd4575c681ceb422f99b6015a/addons/portal/models/mail_message.py#L30-L32 Problem is that, when creating the message with the attachement, `subtype_id` is not defined. OPW-2440069 closes odoo/odoo#66906 X-original-commit: b05a72d3cc181c601807b5e633766d504e06bb3a Signed-off-by: Adrien Widart <adwid@users.noreply.github.com>
272 lines
12 KiB
Python
272 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):
|
|
|
|
@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:
|
|
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)]
|