From a97abef2d27521b7fbba2e9ea47dfc9233c0e014 Mon Sep 17 00:00:00 2001 From: tsm-odoo Date: Fri, 6 Oct 2023 16:31:17 +0200 Subject: [PATCH] [IMP] im_livechat, mail: allow livechat visitors to upload attachments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- addons/im_livechat/__manifest__.py | 1 + addons/im_livechat/controllers/__init__.py | 1 + addons/im_livechat/controllers/attachment.py | 22 ++++++++ .../im_livechat/controllers/cors/__init__.py | 2 + .../controllers/cors/attachment.py | 15 ++++++ addons/im_livechat/controllers/cors/binary.py | 33 ++++++++++++ .../im_livechat/controllers/cors/webclient.py | 2 +- .../static/src/composer/composer_patch.js | 4 -- .../src/embed/core/disabled_features.js | 17 ++----- .../src/embed/core/thread_model_patch.js | 23 ++++++--- .../src/embed/cors/attachment_model_patch.js | 21 ++++++++ .../cors/attachment_upload_service_patch.js | 17 +++++++ .../src/embed/cors/livechat_routing_map.js | 1 + .../static/tests/composer_patch_tests.js | 35 +------------ addons/im_livechat/tests/__init__.py | 1 + .../tests/test_upload_attachment.py | 36 +++++++++++++ .../views/im_livechat_channel_templates.xml | 4 +- addons/mail/controllers/attachment.py | 10 ++-- addons/mail/models/discuss/discuss_channel.py | 2 + addons/mail/models/discuss/ir_attachment.py | 15 +----- addons/mail/models/ir_attachment.py | 4 -- addons/mail/models/mail_thread.py | 6 ++- .../core/common/attachment_upload_service.js | 6 ++- .../mail/static/src/core/common/composer.js | 19 ++++--- .../static/src/core/common/dropzone_hook.js | 4 +- .../discuss/core/common/attachment_panel.js | 21 +++++++- .../discuss/core/common/attachment_panel.xml | 7 +++ .../common/attachment_upload_service_patch.js | 24 +++++++++ .../src/discuss/core/common/composer_patch.js | 16 ++++++ .../discuss/core/common/thread_model_patch.js | 4 ++ .../discuss/core/attachment_panel_tests.js | 20 ++++++++ .../mock_server/models/discuss_channel.js | 1 + .../mock_server/models/mail_message.js | 2 +- .../helpers/mock_server/models/res_partner.js | 1 + addons/mail/tests/discuss/__init__.py | 1 + .../discuss/test_discuss_channel_as_guest.py | 2 + .../mail/tests/discuss/test_toggle_upload.py | 51 +++++++++++++++++++ .../tests/test_performance.py | 12 +++++ .../static/src/views/fields/file_handler.js | 3 +- 39 files changed, 369 insertions(+), 97 deletions(-) create mode 100644 addons/im_livechat/controllers/attachment.py create mode 100644 addons/im_livechat/controllers/cors/attachment.py create mode 100644 addons/im_livechat/controllers/cors/binary.py create mode 100644 addons/im_livechat/static/src/embed/cors/attachment_model_patch.js create mode 100644 addons/im_livechat/static/src/embed/cors/attachment_upload_service_patch.js create mode 100644 addons/im_livechat/tests/test_upload_attachment.py create mode 100644 addons/mail/static/src/discuss/core/common/attachment_upload_service_patch.js create mode 100644 addons/mail/static/src/discuss/core/common/composer_patch.js create mode 100644 addons/mail/tests/discuss/test_toggle_upload.py diff --git a/addons/im_livechat/__manifest__.py b/addons/im_livechat/__manifest__.py index 5e7bbebda10..c79a7e56b3f 100644 --- a/addons/im_livechat/__manifest__.py +++ b/addons/im_livechat/__manifest__.py @@ -124,6 +124,7 @@ Help your customers with this chat, and analyse their feedback. 'web/static/src/libs/pdfjs.js', 'web/static/src/views/fields/formatters.js', 'web/static/src/views/fields/file_handler.*', + 'web/static/src/scss/mimetypes.scss', 'bus/static/src/*.js', 'bus/static/src/services/**/*.js', 'bus/static/src/workers/websocket_worker.js', diff --git a/addons/im_livechat/controllers/__init__.py b/addons/im_livechat/controllers/__init__.py index bf729810686..29228c6c1c8 100644 --- a/addons/im_livechat/controllers/__init__.py +++ b/addons/im_livechat/controllers/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import attachment from . import chatbot from . import main from . import webclient diff --git a/addons/im_livechat/controllers/attachment.py b/addons/im_livechat/controllers/attachment.py new file mode 100644 index 00000000000..4caa39d7d92 --- /dev/null +++ b/addons/im_livechat/controllers/attachment.py @@ -0,0 +1,22 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _ +from odoo.http import route, request +from odoo.addons.mail.controllers.attachment import AttachmentController +from odoo.exceptions import AccessError +from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context + + +class LivechatAttachmentController(AttachmentController): + @route() + @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 thread.channel_type == "livechat" + and not thread.livechat_active + and not request.env.user._is_internal() + ): + raise AccessError(_("You are not allowed to upload attachments on this channel.")) + return super().mail_attachment_upload(ufile, thread_id, thread_model, is_pending, **kwargs) diff --git a/addons/im_livechat/controllers/cors/__init__.py b/addons/im_livechat/controllers/cors/__init__.py index 2389fd39d69..96b7454e235 100644 --- a/addons/im_livechat/controllers/cors/__init__.py +++ b/addons/im_livechat/controllers/cors/__init__.py @@ -1,5 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import attachment +from . import binary from . import channel from . import link_preview from . import main diff --git a/addons/im_livechat/controllers/cors/attachment.py b/addons/im_livechat/controllers/cors/attachment.py new file mode 100644 index 00000000000..2d2fabf156c --- /dev/null +++ b/addons/im_livechat/controllers/cors/attachment.py @@ -0,0 +1,15 @@ +from odoo.http import route +from odoo.addons.mail.controllers.attachment import AttachmentController +from odoo.addons.im_livechat.tools.misc import force_guest_env + + +class LivechatAttachmentController(AttachmentController): + @route("/im_livechat/cors/attachment/upload", auth="public", cors="*", csrf=False) + def im_livechat_attachment_upload(self, guest_token, ufile, thread_id, thread_model, is_pending=False, **kwargs): + force_guest_env(guest_token) + return self.mail_attachment_upload(ufile, thread_id, thread_model, is_pending, **kwargs) + + @route("/im_livechat/cors/attachment/delete", methods=["POST"], type="json", auth="public", cors="*") + def im_livechat_attachment_delete(self, guest_token, attachment_id, access_token=None): + force_guest_env(guest_token) + return self.mail_attachment_delete(attachment_id, access_token) diff --git a/addons/im_livechat/controllers/cors/binary.py b/addons/im_livechat/controllers/cors/binary.py new file mode 100644 index 00000000000..5e55381b66a --- /dev/null +++ b/addons/im_livechat/controllers/cors/binary.py @@ -0,0 +1,33 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +from odoo.http import route +from odoo.addons.mail.controllers.discuss.binary import BinaryController +from odoo.addons.im_livechat.tools.misc import force_guest_env + + +class LivechatBinaryController(BinaryController): + @route( + "/im_livechat/cors/channel//attachment/", + methods=["GET"], + type="http", + auth="public", + cors="*", + ) + def livechat_channel_attachment(self, guest_token, channel_id, attachment_id, download=None, **kwargs): + force_guest_env(guest_token) + return self.discuss_channel_attachment(channel_id, attachment_id, download, **kwargs) + + @route( + [ + "/im_livechat/cors/channel//image/", + "/im_livechat/cors/channel//image//x", + ], + methods=["GET"], + type="http", + auth="public", + cors="*" + ) + def livechat_fetch_image(self, guest_token, channel_id, attachment_id, width=0, height=0, **kwargs): + force_guest_env(guest_token) + return self.fetch_image(channel_id, attachment_id, width, height, **kwargs) diff --git a/addons/im_livechat/controllers/cors/webclient.py b/addons/im_livechat/controllers/cors/webclient.py index 859762f0973..436413b954d 100644 --- a/addons/im_livechat/controllers/cors/webclient.py +++ b/addons/im_livechat/controllers/cors/webclient.py @@ -9,4 +9,4 @@ class WebClient(WebclientController): @route("/im_livechat/cors/init_messaging", methods=["POST"], type="json", auth="public", cors="*") def livechat_init_messaging(self, guest_token): force_guest_env(guest_token) - return super().mail_init_messaging() + return self.mail_init_messaging() diff --git a/addons/im_livechat/static/src/composer/composer_patch.js b/addons/im_livechat/static/src/composer/composer_patch.js index 5620709b42f..e15b764882f 100644 --- a/addons/im_livechat/static/src/composer/composer_patch.js +++ b/addons/im_livechat/static/src/composer/composer_patch.js @@ -5,10 +5,6 @@ import { Composer } from "@mail/core/common/composer"; import { patch } from "@web/core/utils/patch"; patch(Composer.prototype, { - get allowUpload() { - return this.thread?.type !== "livechat" && super.allowUpload; - }, - onKeydown(ev) { super.onKeydown(ev); if ( diff --git a/addons/im_livechat/static/src/embed/core/disabled_features.js b/addons/im_livechat/static/src/embed/core/disabled_features.js index c8967f140df..25b1e7e09a8 100644 --- a/addons/im_livechat/static/src/embed/core/disabled_features.js +++ b/addons/im_livechat/static/src/embed/core/disabled_features.js @@ -1,7 +1,6 @@ /* @odoo-module */ -import { Composer } from "@mail/core/common/composer"; -import { Store } from "@mail/core/common/store_service"; +import { messageActionsRegistry } from "@mail/core/common/message_actions"; import { threadActionsRegistry } from "@mail/core/common/thread_actions"; import { Thread } from "@mail/core/common/thread_model"; import { ThreadService } from "@mail/core/common/thread_service"; @@ -9,9 +8,10 @@ import { ThreadService } from "@mail/core/common/thread_service"; import { patch } from "@web/core/utils/patch"; import { SESSION_STATE } from "./livechat_service"; -patch(Composer.prototype, { - get allowUpload() { - return false; +const downloadFilesAction = messageActionsRegistry.get("download_files"); +patch(downloadFilesAction, { + condition(component) { + return component.message.originThread.type !== "livechat" && super.condition(component); }, }); @@ -32,13 +32,6 @@ patch(ThreadService.prototype, { }, }); -patch(Store.prototype, { - setup() { - super.setup(...arguments); - this.hasLinkPreviewFeature = false; - }, -}); - const allowedThreadActions = new Set(["fold-chat-window", "close", "restart", "settings"]); for (const [actionName] of threadActionsRegistry.getEntries()) { if (!allowedThreadActions.has(actionName)) { diff --git a/addons/im_livechat/static/src/embed/core/thread_model_patch.js b/addons/im_livechat/static/src/embed/core/thread_model_patch.js index 6aa726ed367..de9955886ec 100644 --- a/addons/im_livechat/static/src/embed/core/thread_model_patch.js +++ b/addons/im_livechat/static/src/embed/core/thread_model_patch.js @@ -16,15 +16,22 @@ patch(Thread, { const chatbotService = this.env.services["im_livechat.chatbot"]; const messageService = this.env.services["mail.message"]; if (thread.type === "livechat" && isUnknown) { - onChange(thread, ["state", "seen_message_id", "message_unread_counter"], () => { - if (![SESSION_STATE.CLOSED, SESSION_STATE.NONE].includes(livechatService.state)) { - livechatService.updateSession({ - state: thread.state, - seen_message_id: thread.seen_message_id, - channel: thread.channel, - }); + onChange( + thread, + ["state", "seen_message_id", "message_unread_counter", "allow_public_upload"], + () => { + if ( + ![SESSION_STATE.CLOSED, SESSION_STATE.NONE].includes(livechatService.state) + ) { + livechatService.updateSession({ + state: thread.state, + seen_message_id: thread.seen_message_id, + channel: thread.channel, + allow_public_upload: thread.allow_public_upload, + }); + } } - }); + ); if (chatbotService.isChatbotThread(thread)) { thread.chatbotTypingMessage = { id: messageService.getNextTemporaryId(), diff --git a/addons/im_livechat/static/src/embed/cors/attachment_model_patch.js b/addons/im_livechat/static/src/embed/cors/attachment_model_patch.js new file mode 100644 index 00000000000..d95f393e2a9 --- /dev/null +++ b/addons/im_livechat/static/src/embed/cors/attachment_model_patch.js @@ -0,0 +1,21 @@ +/* @odoo-module */ + +import { Attachment } from "@mail/core/common/attachment_model"; +import { patch } from "@web/core/utils/patch"; + +patch(Attachment.prototype, { + get urlQueryParams() { + return { + ...super.urlQueryParams, + guest_token: this._store.env.services["im_livechat.livechat"].guestToken, + }; + }, + get urlRoute() { + if (!this.accessToken && this.originThread?.model === "discuss.channel") { + return this.isImage + ? `/im_livechat/cors/channel/${this.originThread.id}/image/${this.id}` + : `/im_livechat/cors/channel/${this.originThread.id}/attachment/${this.id}`; + } + return super.urlRoute; + }, +}); diff --git a/addons/im_livechat/static/src/embed/cors/attachment_upload_service_patch.js b/addons/im_livechat/static/src/embed/cors/attachment_upload_service_patch.js new file mode 100644 index 00000000000..0edb8f451f6 --- /dev/null +++ b/addons/im_livechat/static/src/embed/cors/attachment_upload_service_patch.js @@ -0,0 +1,17 @@ +/* @odoo-module */ + +import { AttachmentUploadService } from "@mail/core/common/attachment_upload_service"; + +import { patch } from "@web/core/utils/patch"; +import { session } from "@web/session"; + +patch(AttachmentUploadService.prototype, { + get uploadURL() { + return `${session.origin}/im_livechat/cors/attachment/upload`; + }, + + _makeFormData() { + const formData = super._makeFormData(...arguments); + formData.append("guest_token", this.env.services["im_livechat.livechat"].guestToken); + }, +}); diff --git a/addons/im_livechat/static/src/embed/cors/livechat_routing_map.js b/addons/im_livechat/static/src/embed/cors/livechat_routing_map.js index 36bb0965e21..609e22ebac8 100644 --- a/addons/im_livechat/static/src/embed/cors/livechat_routing_map.js +++ b/addons/im_livechat/static/src/embed/cors/livechat_routing_map.js @@ -18,6 +18,7 @@ livechatRoutingMap "/discuss/channel/set_last_seen_message", "/im_livechat/cors/channel/set_last_seen_message" ) + .add("/mail/attachment/delete", "/im_livechat/cors/attachment/delete") .add("/discuss/channel/ping", "/im_livechat/cors/channel/ping") .add("/mail/init_messaging", "/im_livechat/cors/init_messaging") .add("/mail/link_preview", "/im_livechat/cors/link_preview") diff --git a/addons/im_livechat/static/tests/composer_patch_tests.js b/addons/im_livechat/static/tests/composer_patch_tests.js index e71e0ada0a5..e73b614a8e3 100644 --- a/addons/im_livechat/static/tests/composer_patch_tests.js +++ b/addons/im_livechat/static/tests/composer_patch_tests.js @@ -5,43 +5,10 @@ import { startServer } from "@bus/../tests/helpers/mock_python_environment"; import { Command } from "@mail/../tests/helpers/command"; import { start } from "@mail/../tests/helpers/test_utils"; -import { click, contains, createFile, dragenterFiles, insertText } from "@web/../tests/utils"; +import { click, contains, insertText } from "@web/../tests/utils"; QUnit.module("composer (patch)"); -QUnit.test("No add attachments button", async () => { - const pyEnv = await startServer(); - const channelId = pyEnv["discuss.channel"].create({ - name: "Livechat 1", - channel_type: "livechat", - }); - const { openDiscuss } = await start(); - await openDiscuss(channelId); - await contains(".o-mail-Composer"); - await contains("button[title='Attach files']", { count: 0 }); -}); - -QUnit.test("Attachment upload via drag and drop disabled", async () => { - const pyEnv = await startServer(); - const channelId = pyEnv["discuss.channel"].create({ - name: "Livechat 1", - channel_type: "livechat", - }); - const { openDiscuss } = await start(); - await openDiscuss(channelId); - await contains(".o-mail-Composer"); - const files = [ - await createFile({ - content: "hello, world", - contentType: "text/plain", - name: "text3.txt", - }), - ]; - await dragenterFiles(".o-mail-Composer-input", files); - // weak test: no guarantee that we waited long enough for the potential dropzone to show - await contains(".o-mail-Dropzone", { count: 0 }); -}); - QUnit.test("Can execute help command on livechat channels", async (assert) => { const pyEnv = await startServer(); const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" }); diff --git a/addons/im_livechat/tests/__init__.py b/addons/im_livechat/tests/__init__.py index f3339ab66b0..4f6047dc3f1 100644 --- a/addons/im_livechat/tests/__init__.py +++ b/addons/im_livechat/tests/__init__.py @@ -12,3 +12,4 @@ from . import test_im_livechat_report from . import test_im_livechat_support_page from . import test_js from . import test_message +from . import test_upload_attachment diff --git a/addons/im_livechat/tests/test_upload_attachment.py b/addons/im_livechat/tests/test_upload_attachment.py new file mode 100644 index 00000000000..073aef87875 --- /dev/null +++ b/addons/im_livechat/tests/test_upload_attachment.py @@ -0,0 +1,36 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.tests.common import tagged, HttpCase +from odoo.tools import mute_logger, file_open + + +@tagged("post_install", "-at_install") +class TestUploadAttachment(HttpCase): + def test_visitor_cannot_upload_on_closed_livechat(self): + self.authenticate(None, None) + operator = self.env["res.users"].create({"name": "Operator", "login": "operator"}) + self.env["bus.presence"].create({"user_id": operator.id, "status": "online"}) + livechat_channel = self.env["im_livechat.channel"].create( + {"name": "Test Livechat Channel", "user_ids": [operator.id]} + ) + channel_info = self.make_jsonrpc_request( + "/im_livechat/get_session", + { + "anonymous_name": "Visitor", + "channel_id": livechat_channel.id, + "persisted": True, + }, + ) + self.make_jsonrpc_request("/im_livechat/visitor_leave_session", {"uuid": channel_info["uuid"]}) + with mute_logger("odoo.http"), file_open("addons/web/__init__.py") as file: + response = self.url_open( + "/mail/attachment/upload", + { + "csrf_token": http.Request.csrf_token(self), + "thread_id": channel_info["id"], + "thread_model": "discuss.channel", + }, + files={"ufile": file}, + ) + self.assertEqual(response.status_code, 403) diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml index ece093833eb..cc1a2c4da3f 100644 --- a/addons/im_livechat/views/im_livechat_channel_templates.xml +++ b/addons/im_livechat/views/im_livechat_channel_templates.xml @@ -89,7 +89,9 @@