Files
odoo_source/addons/mail/controllers/attachment.py
T
tsm-odoo a97abef2d2 [IMP] im_livechat, mail: allow livechat visitors to upload attachments
Since [1], live chat visitors are using the mail guest system for
authentication. With this change, visitors are allowed to reach the
attachment upload routes (even if nothing allows it in the frontend
for now). This commit restricts attachment upload for guest and portal
users with the `allow_visitor_upload` field that can be toggled.

At the same time, this commit enables file upload when authorized
on the frontend and for cross origin live chats.

[1]: odoo#129770

task-3332628

closes odoo/odoo#137574

Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
2023-10-06 18:18:29 +00:00

121 lines
5.5 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import io
import logging
import zipfile
from werkzeug.exceptions import NotFound
from odoo import _, http
from odoo.exceptions import AccessError
from odoo.http import request, content_disposition
from odoo.tools import consteq
from ..models.discuss.mail_guest import add_guest_to_context
logger = logging.getLogger(__name__)
class AttachmentController(http.Controller):
def _make_zip(self, name, attachments):
streams = (request.env['ir.binary']._get_stream_from(record, 'raw') for record in attachments)
# TODO: zip on-the-fly while streaming instead of loading the
# entire zip in memory and sending it all at once.
stream = io.BytesIO()
try:
with zipfile.ZipFile(stream, 'w') as attachment_zip:
for binary_stream in streams:
if not binary_stream:
continue
attachment_zip.writestr(
binary_stream.download_name,
binary_stream.read(),
compress_type=zipfile.ZIP_DEFLATED
)
except zipfile.BadZipFile:
logger.exception("BadZipfile exception")
content = stream.getvalue()
headers = [
('Content-Type', 'zip'),
('X-Content-Type-Options', 'nosniff'),
('Content-Length', len(content)),
('Content-Disposition', content_disposition(name))
]
return request.make_response(content, headers)
@http.route("/mail/attachment/upload", methods=["POST"], type="http", auth="public")
@add_guest_to_context
def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs):
thread = request.env[thread_model]._get_from_context_or_raise(int(thread_id))
if thread_model == "discuss.channel" and not thread.allow_public_upload and not request.env.user._is_internal():
raise AccessError(_("You are not allowed to upload attachments on this channel."))
vals = {
"name": ufile.filename,
"raw": ufile.read(),
"res_id": int(thread_id),
"res_model": thread_model,
}
if is_pending and is_pending != "false":
# Add this point, the message related to the uploaded file does
# not exist yet, so we use those placeholder values instead.
vals.update(
{
"res_id": 0,
"res_model": "mail.compose.message",
}
)
if thread.env.user.share:
# Only generate the access token if absolutely necessary (= not for internal user).
vals["access_token"] = thread.env["ir.attachment"]._generate_access_token()
try:
attachment = thread.env["ir.attachment"].create(vals)
attachment._post_add_create(**kwargs)
attachmentData = attachment._attachment_format()[0]
if attachment.access_token:
attachmentData["accessToken"] = attachment.access_token
except AccessError:
attachmentData = {"error": _("You are not allowed to upload an attachment here.")}
return request.make_json_response(attachmentData)
@http.route("/mail/attachment/delete", methods=["POST"], type="json", auth="public")
@add_guest_to_context
def mail_attachment_delete(self, attachment_id, access_token=None):
attachment_sudo = request.env["ir.attachment"].browse(int(attachment_id)).sudo().exists()
guest = request.env["mail.guest"]._get_guest_from_context()
message_sudo = guest.env["mail.message"].sudo().search([("attachment_ids", "in", attachment_sudo.ids)], limit=1)
if not attachment_sudo:
target = request.env.user.partner_id
request.env["bus.bus"]._sendone(target, "ir.attachment/delete", {"id": attachment_id})
return
if not request.env.user.share:
# Check through standard access rights/rules for internal users.
attachment_sudo.sudo(False)._delete_and_notify(message_sudo)
return
# For non-internal users 2 cases are supported:
# - Either the attachment is linked to a message: verify the request is made by the author of the message (portal user or guest).
# - Either a valid access token is given: also verify the message is pending (because unfortunately in portal a token is also provided to guest for viewing others' attachments).
if message_sudo:
if not message_sudo.is_current_user_or_guest_author:
raise NotFound()
else:
if (
not access_token
or not attachment_sudo.access_token
or not consteq(access_token, attachment_sudo.access_token)
):
raise NotFound()
if attachment_sudo.res_model != "mail.compose.message" or attachment_sudo.res_id != 0:
raise NotFound()
attachment_sudo._delete_and_notify(message_sudo)
@http.route(['/mail/attachment/zip'], methods=["POST"], type="http", auth="public")
def mail_attachment_get_zip(self, file_ids, zip_name, **kw):
"""route to get the zip file of the attachments.
:param file_ids: ids of the files to zip.
:param zip_name: name of the zip file.
"""
ids_list = list(map(int, file_ids.split(',')))
attachments = request.env['ir.attachment'].browse(ids_list)
return self._make_zip(zip_name, attachments)