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 @@