oca-ocb-core/odoo-bringout-oca-ocb-mail/mail/controllers/attachment.py
Ernad Husremovic 2d3ee4855a 19.0 vanilla
2026-03-09 09:30:27 +01:00

169 lines
7.2 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import io
import logging
import zipfile
from werkzeug.exceptions import NotFound, UnsupportedMediaType
from odoo import _, http
from odoo.addons.mail.controllers.thread import ThreadController
from odoo.exceptions import AccessError, UserError
from odoo.http import request, content_disposition
from odoo.addons.mail.tools.discuss import add_guest_to_context, Store
from odoo.tools.misc import file_open
from odoo.tools.pdf import DependencyError, PdfReadError, extract_page
logger = logging.getLogger(__name__)
class AttachmentController(ThreadController):
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 = self._get_thread_with_access_for_post(thread_model, thread_id, **kwargs)
if not thread:
raise NotFound()
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",
}
)
try:
# sudo: ir.attachment - posting a new attachment on an accessible thread
attachment = request.env["ir.attachment"].sudo().create(vals)
attachment._post_add_create(**kwargs)
res = {
"data": {
"store_data": Store().add(
attachment,
extra_fields=request.env["ir.attachment"]._get_store_ownership_fields(),
).get_result(),
"attachment_id": attachment.id,
}
}
except AccessError:
res = {"error": _("You are not allowed to upload an attachment here.")}
return request.make_json_response(res)
@http.route("/mail/attachment/delete", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def mail_attachment_delete(self, attachment_id, access_token=None):
attachment = request.env["ir.attachment"].browse(int(attachment_id)).exists()
if not attachment or not attachment._has_attachments_ownership([access_token]):
request.env.user._bus_send("ir.attachment/delete", {"id": attachment_id})
raise NotFound()
message = request.env["mail.message"].sudo().search(
[("attachment_ids", "in", attachment.ids)], limit=1)
# sudo: ir.attachment: access is validated with _has_attachments_ownership
attachment.sudo()._delete_and_notify(message)
@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)
@http.route(
"/mail/attachment/pdf_first_page/<int:attachment_id>",
auth="public",
methods=["GET"],
readonly=True,
type="http",
)
@add_guest_to_context
def mail_attachment_pdf_first_page(self, attachment_id, access_token=None):
"""Returns the first page of a pdf."""
attachment = request.env["ir.attachment"].browse(int(attachment_id)).exists()
if not attachment or (
not attachment.has_access("read")
and not attachment._has_attachments_ownership([access_token])
):
raise request.not_found()
# sudo: ir.attachment: access check is done above, sudo necessary for guests
return self._get_pdf_first_page_response(attachment.sudo())
@http.route(
"/mail/attachment/update_thumbnail",
auth="public",
methods=["POST"],
type="jsonrpc",
)
@add_guest_to_context
def mail_attachement_update_thumbnail(self, attachment_id, thumbnail=None, access_token=None):
"""Updates the thumbnail of an attachment."""
attachment = request.env["ir.attachment"].browse(int(attachment_id)).exists()
if not attachment or (
not attachment.has_access("write")
and not attachment._has_attachments_ownership([access_token])
):
raise request.not_found()
# sudo: ir.attachment: access check is done above, sudo necessary for guests
attachment_sudo = attachment.sudo()
if attachment_sudo.mimetype != "application/pdf":
raise UserError(request.env._("Only PDF files can have thumbnail."))
if not thumbnail:
with file_open("web/static/img/mimetypes/unknown.svg") as unknown_svg:
thumbnail = base64.b64encode(unknown_svg.read().encode())
attachment_sudo.thumbnail = thumbnail
Store(bus_channel=attachment_sudo).add(attachment_sudo, ["has_thumbnail"]).bus_send()
def _get_pdf_first_page_response(self, attachment):
try:
page_stream = extract_page(attachment, 0)
except (PdfReadError, DependencyError, UnicodeDecodeError) as e:
raise UnsupportedMediaType() from e
if not page_stream:
raise UnsupportedMediaType()
content = page_stream.getvalue()
headers = [
("Content-Type", "attachment/pdf"),
("X-Content-Type-Options", "nosniff"),
("Content-Length", len(content)),
]
if attachment.name:
headers.append(("Content-Disposition", content_disposition(attachment.name)))
return request.make_response(content, headers)