From bd4e418fc11b8d4f4ba93161b974d7fb7e418458 Mon Sep 17 00:00:00 2001 From: tsm-odoo Date: Tue, 3 Oct 2023 13:25:06 +0200 Subject: [PATCH] [IMP] im_livechat, mail: allow CORS livechat actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR makes several actions available for the embed livechat such as: - calls - message edition - message reactions - link previews task-3523930 closes odoo/odoo#136618 Signed-off-by: Sébastien Theys (seb) --- addons/im_livechat/__init__.py | 1 + addons/im_livechat/__manifest__.py | 5 + addons/im_livechat/controllers/__init__.py | 1 + .../im_livechat/controllers/cors/__init__.py | 9 ++ .../im_livechat/controllers/cors/channel.py | 17 ++++ .../controllers/cors/link_preview.py | 17 ++++ addons/im_livechat/controllers/cors/main.py | 21 +++++ .../controllers/cors/message_reaction.py | 12 +++ addons/im_livechat/controllers/cors/rtc.py | 32 +++++++ addons/im_livechat/controllers/cors/thread.py | 19 ++++ .../im_livechat/controllers/cors/webclient.py | 12 +++ addons/im_livechat/controllers/main.py | 33 ++----- .../im_livechat/static/src/embed/cors/boot.js | 45 +++++++++ .../src/embed/cors/livechat_routing_map.js | 39 ++++++++ .../static/src/embed/external/boot.js | 26 +----- addons/im_livechat/tests/__init__.py | 1 + .../im_livechat/tests/test_cors_livechat.py | 93 +++++++++++++++++++ addons/im_livechat/tools/__init__.py | 3 + addons/im_livechat/tools/misc.py | 25 +++++ addons/mail/models/discuss/mail_guest.py | 35 ++++--- 20 files changed, 382 insertions(+), 64 deletions(-) create mode 100644 addons/im_livechat/controllers/cors/__init__.py create mode 100644 addons/im_livechat/controllers/cors/channel.py create mode 100644 addons/im_livechat/controllers/cors/link_preview.py create mode 100644 addons/im_livechat/controllers/cors/main.py create mode 100644 addons/im_livechat/controllers/cors/message_reaction.py create mode 100644 addons/im_livechat/controllers/cors/rtc.py create mode 100644 addons/im_livechat/controllers/cors/thread.py create mode 100644 addons/im_livechat/controllers/cors/webclient.py create mode 100644 addons/im_livechat/static/src/embed/cors/boot.js create mode 100644 addons/im_livechat/static/src/embed/cors/livechat_routing_map.js create mode 100644 addons/im_livechat/tests/test_cors_livechat.py create mode 100644 addons/im_livechat/tools/__init__.py create mode 100644 addons/im_livechat/tools/misc.py diff --git a/addons/im_livechat/__init__.py b/addons/im_livechat/__init__.py index b8980e258a2..d66f8f1bf1c 100644 --- a/addons/im_livechat/__init__.py +++ b/addons/im_livechat/__init__.py @@ -3,3 +3,4 @@ from . import controllers from . import models from . import report from . import demo +from . import tools diff --git a/addons/im_livechat/__manifest__.py b/addons/im_livechat/__manifest__.py index b4d6764cc2f..bc078687a75 100644 --- a/addons/im_livechat/__manifest__.py +++ b/addons/im_livechat/__manifest__.py @@ -107,6 +107,7 @@ Help your customers with this chat, and analyse their feedback. 'im_livechat/static/src/embed/livechat_data.js', ('remove', 'im_livechat/static/src/embed/frontend/**/*'), ('remove', 'im_livechat/static/src/embed/external/**/*'), + ('remove', 'im_livechat/static/src/embed/cors/**/*'), ], 'im_livechat.assets_embed': [ 'web/static/lib/jquery/jquery.js', @@ -132,6 +133,10 @@ Help your customers with this chat, and analyse their feedback. ('include', 'im_livechat.assets_core'), 'im_livechat/static/src/embed/external/**/*', ], + 'im_livechat.assets_cors': [ + ('include', 'im_livechat.assets_embed'), + 'im_livechat/static/src/embed/cors/**/*', + ], 'im_livechat.embed_test_assets': [ ('include', 'web.tests_assets'), ('remove', 'web/static/tests/mock_server_tests.js'), diff --git a/addons/im_livechat/controllers/__init__.py b/addons/im_livechat/controllers/__init__.py index 1e383b124d7..bf729810686 100644 --- a/addons/im_livechat/controllers/__init__.py +++ b/addons/im_livechat/controllers/__init__.py @@ -4,3 +4,4 @@ from . import chatbot from . import main from . import webclient +from . import cors diff --git a/addons/im_livechat/controllers/cors/__init__.py b/addons/im_livechat/controllers/cors/__init__.py new file mode 100644 index 00000000000..2389fd39d69 --- /dev/null +++ b/addons/im_livechat/controllers/cors/__init__.py @@ -0,0 +1,9 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import channel +from . import link_preview +from . import main +from . import message_reaction +from . import rtc +from . import thread +from . import webclient diff --git a/addons/im_livechat/controllers/cors/channel.py b/addons/im_livechat/controllers/cors/channel.py new file mode 100644 index 00000000000..6ed167f7242 --- /dev/null +++ b/addons/im_livechat/controllers/cors/channel.py @@ -0,0 +1,17 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import route +from odoo.addons.mail.controllers.discuss.channel import ChannelController +from odoo.addons.im_livechat.tools.misc import force_guest_env + + +class LivechatChannelController(ChannelController): + @route("/im_livechat/cors/channel/messages", methods=["POST"], type="json", auth="public", cors="*") + def livechat_channel_messages(self, guest_token, channel_id, before=None, after=None, limit=30, around=None): + force_guest_env(guest_token) + return self.discuss_channel_messages(channel_id, before, after, limit, around) + + @route("/im_livechat/cors/channel/set_last_seen_message", methods=["POST"], type="json", auth="public", cors="*") + def livechat_channel_mark_as_seen(self, guest_token, channel_id, last_message_id, allow_older=False): + force_guest_env(guest_token) + return self.discuss_channel_mark_as_seen(channel_id, last_message_id, allow_older) diff --git a/addons/im_livechat/controllers/cors/link_preview.py b/addons/im_livechat/controllers/cors/link_preview.py new file mode 100644 index 00000000000..7924593884a --- /dev/null +++ b/addons/im_livechat/controllers/cors/link_preview.py @@ -0,0 +1,17 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import route +from odoo.addons.mail.controllers.link_preview import LinkPreviewController +from odoo.addons.im_livechat.tools.misc import force_guest_env + + +class LivechatLinkPreviewController(LinkPreviewController): + @route("/im_livechat/cors/link_preview", methods=["POST"], type="json", auth="public", cors="*") + def livechat_link_preview(self, guest_token, message_id, clear=None): + force_guest_env(guest_token) + self.mail_link_preview(message_id, clear) + + @route("/im_livechat/cors/link_preview/delete", methods=["POST"], type="json", auth="public", cors="*") + def livechat_link_preview_delete(self, guest_token, link_preview_ids): + force_guest_env(guest_token) + self.mail_link_preview_delete(link_preview_ids) diff --git a/addons/im_livechat/controllers/cors/main.py b/addons/im_livechat/controllers/cors/main.py new file mode 100644 index 00000000000..0d25b7c63db --- /dev/null +++ b/addons/im_livechat/controllers/cors/main.py @@ -0,0 +1,21 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import route +from odoo.addons.im_livechat.controllers.main import LivechatController +from odoo.addons.im_livechat.tools.misc import downgrade_to_public_user, force_guest_env + + +class CorsLivechatController(LivechatController): + @route("/im_livechat/cors/visitor_leave_session", type="json", auth="public", cors="*") + def cors_visitor_leave_session(self, guest_token, uuid): + force_guest_env(guest_token) + self.visitor_leave_session(uuid) + + @route("/im_livechat/cors/get_session", methods=["POST"], type="json", auth="public", cors="*") + def cors_get_session( + self, channel_id, anonymous_name, previous_operator_id=None, chatbot_script_id=None, persisted=True, **kwargs + ): + downgrade_to_public_user() + return self.get_session( + channel_id, anonymous_name, previous_operator_id, chatbot_script_id, persisted, **kwargs + ) diff --git a/addons/im_livechat/controllers/cors/message_reaction.py b/addons/im_livechat/controllers/cors/message_reaction.py new file mode 100644 index 00000000000..0e1c81c7a69 --- /dev/null +++ b/addons/im_livechat/controllers/cors/message_reaction.py @@ -0,0 +1,12 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import route +from odoo.addons.mail.controllers.message_reaction import MessageReactionController +from odoo.addons.im_livechat.tools.misc import force_guest_env + + +class LivechatMessageReactionController(MessageReactionController): + @route("/im_livechat/cors/message/reaction", methods=["POST"], type="json", auth="public", cors="*") + def livechat_message_add_reaction(self, guest_token, message_id, content, action): + force_guest_env(guest_token) + return self.mail_message_add_reaction(message_id, content, action) diff --git a/addons/im_livechat/controllers/cors/rtc.py b/addons/im_livechat/controllers/cors/rtc.py new file mode 100644 index 00000000000..9a6259d2809 --- /dev/null +++ b/addons/im_livechat/controllers/cors/rtc.py @@ -0,0 +1,32 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import route +from odoo.addons.mail.controllers.discuss.rtc import RtcController +from odoo.addons.im_livechat.tools.misc import force_guest_env + + +class LivechatRtcController(RtcController): + @route("/im_livechat/cors/rtc/channel/join_call", methods=["POST"], type="json", auth="public", cors="*") + def livechat_channel_call_join(self, guest_token, channel_id, check_rtc_session_ids=None): + force_guest_env(guest_token) + return self.channel_call_join(channel_id, check_rtc_session_ids) + + @route("/im_livechat/cors/rtc/channel/leave_call", methods=["POST"], type="json", auth="public", cors="*") + def livechat_channel_call_leave(self, guest_token, channel_id): + force_guest_env(guest_token) + return self.channel_call_leave(channel_id) + + @route("/im_livechat/cors/rtc/session/update_and_broadcast", methods=["POST"], type="json", auth="public", cors="*") + def livechat_session_update_and_broadcast(self, guest_token, session_id, values): + force_guest_env(guest_token) + self.session_update_and_broadcast(session_id, values) + + @route("/im_livechat/cors/rtc/session/notify_call_members", methods=["POST"], type="json", auth="public", cors="*") + def livechat_session_call_notify(self, guest_token, peer_notifications): + force_guest_env(guest_token) + self.session_call_notify(peer_notifications) + + @route("/im_livechat/cors/channel/ping", methods=["POST"], type="json", auth="public", cors="*") + def livechat_channel_ping(self, guest_token, channel_id, rtc_session_id=None, check_rtc_session_ids=None): + force_guest_env(guest_token) + return self.channel_ping(channel_id, rtc_session_id, check_rtc_session_ids) diff --git a/addons/im_livechat/controllers/cors/thread.py b/addons/im_livechat/controllers/cors/thread.py new file mode 100644 index 00000000000..703b7d889c0 --- /dev/null +++ b/addons/im_livechat/controllers/cors/thread.py @@ -0,0 +1,19 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import route +from odoo.addons.mail.controllers.thread import ThreadController +from odoo.addons.im_livechat.tools.misc import force_guest_env + + +class LivechatThreadController(ThreadController): + @route("/im_livechat/cors/message/post", methods=["POST"], type="json", auth="public", cors="*") + def livechat_message_post(self, guest_token, thread_model, thread_id, post_data, context=None): + force_guest_env(guest_token) + return self.mail_message_post(thread_model, thread_id, post_data, context) + + @route("/im_livechat/cors/message/update_content", methods=["POST"], type="json", auth="public", cors="*") + def livechat_message_update_content( + self, guest_token, message_id, body, attachment_ids, attachment_tokens=None, partner_ids=None + ): + force_guest_env(guest_token) + return self.mail_message_update_content(message_id, body, attachment_ids, attachment_tokens, partner_ids) diff --git a/addons/im_livechat/controllers/cors/webclient.py b/addons/im_livechat/controllers/cors/webclient.py new file mode 100644 index 00000000000..859762f0973 --- /dev/null +++ b/addons/im_livechat/controllers/cors/webclient.py @@ -0,0 +1,12 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import route +from odoo.addons.mail.controllers.webclient import WebclientController +from odoo.addons.im_livechat.tools.misc import force_guest_env + + +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() diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index b1d7da70bd4..4a4c0f9f335 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -3,6 +3,7 @@ from markupsafe import Markup import re from werkzeug.exceptions import NotFound +from urllib.parse import urlsplit from odoo import http, tools, _, release from odoo.http import request @@ -30,7 +31,13 @@ class LivechatController(http.Controller): @http.route('/im_livechat/assets_embed.', type='http', auth='public', cors='*') def assets_embed(self, ext, **kwargs): + # If the request comes from a different origin, we must provide the CORS + # assets to enable the redirection of routes to the CORS controller. + headers = request.httprequest.headers + origin_url = urlsplit(headers.get('referer')) bundle = 'im_livechat.assets_embed' + if origin_url.netloc != headers.get('host') or origin_url.scheme != request.httprequest.scheme: + bundle = 'im_livechat.assets_cors' asset = request.env["ir.qweb"]._get_asset_bundle(bundle) if ext not in ('css', 'js'): raise request.not_found() @@ -143,7 +150,7 @@ class LivechatController(http.Controller): def _get_guest_name(self): return _("Visitor") - @http.route('/im_livechat/get_session', methods=["POST"], type="json", auth='public', cors="*") + @http.route('/im_livechat/get_session', methods=["POST"], type="json", auth='public') @add_guest_to_context def get_session(self, channel_id, anonymous_name, previous_operator_id=None, chatbot_script_id=None, persisted=True, **kwargs): user_id = None @@ -252,20 +259,6 @@ class LivechatController(http.Controller): channel._send_history_message(pid, page_history) return True - @http.route('/im_livechat/notify_typing', type='json', auth='public', cors="*") - def notify_typing(self, uuid, is_typing): - """ Broadcast the typing notification of the website user to other channel members - :param uuid: (string) the UUID of the livechat channel - :param is_typing: (boolean) tells whether the website user is typing or not. - """ - channel = request.env['discuss.channel'].sudo().search([('uuid', '=', uuid)]) - if not channel: - raise NotFound() - channel_member = channel.env['discuss.channel.member'].search([('channel_id', '=', channel.id), ('partner_id', '=', request.env.user.partner_id.id)]) - if not channel_member: - raise NotFound() - channel_member._notify_typing(is_typing=is_typing) - @http.route('/im_livechat/email_livechat_transcript', type='json', auth='public', cors="*") def email_livechat_transcript(self, uuid, email): channel = request.env['discuss.channel'].sudo().search([ @@ -274,7 +267,7 @@ class LivechatController(http.Controller): if channel: channel._email_livechat_transcript(email) - @http.route('/im_livechat/visitor_leave_session', type='json', auth="public", cors="*") + @http.route('/im_livechat/visitor_leave_session', type='json', auth="public") @add_guest_to_context def visitor_leave_session(self, uuid): """ Called when the livechat visitor leaves the conversation. @@ -287,11 +280,3 @@ class LivechatController(http.Controller): channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_context_or_raise(channel_id=discuss_channel.id) channel_member_sudo._rtc_leave_call() discuss_channel._close_livechat_session() - - @http.route(['/im_livechat/chat_history'], type="json", auth="public", cors="*") - def im_livechat_chat_history(self, uuid, last_id=False, limit=20): - channel = request.env["discuss.channel"].sudo().search([('uuid', '=', uuid)], limit=1) - if not channel: - return [] - else: - return channel._channel_fetch_message(last_id, limit) diff --git a/addons/im_livechat/static/src/embed/cors/boot.js b/addons/im_livechat/static/src/embed/cors/boot.js new file mode 100644 index 00000000000..05478c7d7f9 --- /dev/null +++ b/addons/im_livechat/static/src/embed/cors/boot.js @@ -0,0 +1,45 @@ +/* @odoo-module */ + +import { livechatRoutingMap } from "@im_livechat/embed/cors/livechat_routing_map"; + +import { browser } from "@web/core/browser/browser"; +import { jsonrpc } from "@web/core/network/rpc_service"; +import { registry } from "@web/core/registry"; +import { session } from "@web/session"; + +(async function boot() { + const { fetch } = browser; + browser.fetch = function (url, ...args) { + if (!url.match(/^(?:https?:)?\/\//)) { + url = session.origin + url; + } + return fetch(url, ...args); + }; + // Override the rpc service to forward requests to CORS-allowed routes. The + // "guest_token" will be appended to the request parameters for + // authentication. + registry.category("services").add( + "rpc", + { + async: true, + start(env) { + return function rpc(route, params = {}, settings) { + if (route in livechatRoutingMap.content) { + route = livechatRoutingMap.get(route, route); + if (env.services["im_livechat.livechat"]?.guestToken) { + params = { + ...params, + guest_token: env.services["im_livechat.livechat"].guestToken, + }; + } + } + if (!route.match(/^(?:https?:)?\/\//)) { + route = session.origin + route; + } + return jsonrpc(route, params, { bus: env.bus, ...settings }); + }; + }, + }, + { force: true } + ); +})(); 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 new file mode 100644 index 00000000000..36bb0965e21 --- /dev/null +++ b/addons/im_livechat/static/src/embed/cors/livechat_routing_map.js @@ -0,0 +1,39 @@ +/* @odoo-module */ + +import { registry } from "@web/core/registry"; + +/** + * This routing map is used to redirect the requests made by the livechat to + * dedicated CORS-allowed routes. Every route expected to be called by the + * livechat should be added here. Note that this will only be used if the + * livechat is loaded from a different origin than the Odoo server. + * + * @see /im_livechat/embed/cors/boot.js + */ +export const livechatRoutingMap = registry.category("discuss.routing_map"); + +livechatRoutingMap + .add("/discuss/channel/messages", "/im_livechat/cors/channel/messages") + .add( + "/discuss/channel/set_last_seen_message", + "/im_livechat/cors/channel/set_last_seen_message" + ) + .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") + .add("/mail/link_preview/delete", "/im_livechat/cors/link_preview/delete") + .add("/mail/message/post", "/im_livechat/cors/message/post") + .add("/mail/message/reaction", "/im_livechat/cors/message/reaction") + .add("/mail/message/update_content", "/im_livechat/cors/message/update_content") + .add("/mail/rtc/channel/join_call", "/im_livechat/cors/rtc/channel/join_call") + .add("/mail/rtc/channel/leave_call", "/im_livechat/cors/rtc/channel/leave_call") + .add( + "/mail/rtc/session/notify_call_members", + "/im_livechat/cors/rtc/session/notify_call_members" + ) + .add( + "/mail/rtc/session/update_and_broadcast", + "/im_livechat/cors/rtc/session/update_and_broadcast" + ) + .add("/im_livechat/visitor_leave_session", "/im_livechat/cors/visitor_leave_session") + .add("/im_livechat/get_session", "/im_livechat/cors/get_session"); diff --git a/addons/im_livechat/static/src/embed/external/boot.js b/addons/im_livechat/static/src/embed/external/boot.js index 8e92ae75e54..5ca2a0df5a2 100644 --- a/addons/im_livechat/static/src/embed/external/boot.js +++ b/addons/im_livechat/static/src/embed/external/boot.js @@ -1,7 +1,7 @@ /* @odoo-module */ +import { makeRoot, makeShadow } from "@im_livechat/embed/boot_helpers"; import { LivechatButton } from "@im_livechat/embed/core_ui/livechat_button"; -import { makeShadow, makeRoot } from "@im_livechat/embed/boot_helpers"; import { serverUrl } from "@im_livechat/embed/livechat_data"; import { ChatWindowContainer } from "@mail/core/common/chat_window_container"; @@ -9,37 +9,13 @@ import { ChatWindowContainer } from "@mail/core/common/chat_window_container"; import { mount, whenReady } from "@odoo/owl"; import { templates } from "@web/core/assets"; -import { browser } from "@web/core/browser/browser"; import { MainComponentsContainer } from "@web/core/main_components_container"; -import { jsonrpc } from "@web/core/network/rpc_service"; import { registry } from "@web/core/registry"; import { makeEnv, startServices } from "@web/env"; import { session } from "@web/session"; (async function boot() { session.origin = serverUrl; - const { fetch } = browser; - browser.fetch = function (url, ...args) { - if (!url.match(/^(?:https?:)?\/\//)) { - url = session.origin + url; - } - return fetch(url, ...args); - }; - registry.category("services").add( - "rpc", - { - async: true, - start(env) { - return function rpc(route, params = {}, settings = {}) { - if (!route.match(/^(?:https?:)?\/\//)) { - route = session.origin + route; - } - return jsonrpc(route, params, { bus: env.bus, ...settings }); - }; - }, - }, - { force: true } - ); await whenReady(); const mainComponentsRegistry = registry.category("main_components"); mainComponentsRegistry.add("LivechatRoot", { Component: LivechatButton }); diff --git a/addons/im_livechat/tests/__init__.py b/addons/im_livechat/tests/__init__.py index fd5663558d5..f3339ab66b0 100644 --- a/addons/im_livechat/tests/__init__.py +++ b/addons/im_livechat/tests/__init__.py @@ -5,6 +5,7 @@ from . import chatbot_common from . import test_chatbot_form_ui from . import test_chatbot_internals from . import test_digest +from . import test_cors_livechat from . import test_get_discuss_channel from . import test_get_operator from . import test_im_livechat_report diff --git a/addons/im_livechat/tests/test_cors_livechat.py b/addons/im_livechat/tests/test_cors_livechat.py new file mode 100644 index 00000000000..3318ea150a4 --- /dev/null +++ b/addons/im_livechat/tests/test_cors_livechat.py @@ -0,0 +1,93 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged, HttpCase, JsonRpcException + + +@tagged("post_install", "-at_install") +class TestCorsLivechat(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.operator = cls.env["res.users"].create( + { + "name": "Operator", + "login": "operator", + } + ) + cls.env["bus.presence"].create( + { + "user_id": cls.operator.id, + "status": "online", + } + ) + cls.livechat_channel = cls.env["im_livechat.channel"].create( + {"name": "Test Livechat Channel", "user_ids": [cls.operator.id]} + ) + + def test_ignore_user_cookie(self): + self.authenticate("admin", "admin") + channel_info = self.make_jsonrpc_request( + "/im_livechat/cors/get_session", + { + "anonymous_name": "Visitor", + "channel_id": self.livechat_channel.id, + "persisted": True, + }, + ) + channel = self.env["discuss.channel"].browse(channel_info["id"]) + self.assertEqual(channel.channel_member_ids[0].partner_id, self.operator.partner_id) + self.assertFalse(channel.channel_member_ids[1].partner_id) + self.assertTrue(channel.channel_member_ids[1].guest_id) + + def test_ignore_guest_cookie(self): + guest = self.env["mail.guest"].create({"name": "Visitor"}) + channel_info = self.make_jsonrpc_request( + "/im_livechat/cors/get_session", + { + "anonymous_name": "Visitor", + "channel_id": self.livechat_channel.id, + "persisted": True, + }, + headers={"Cookie": f"{guest._cookie_name}={guest.id}{guest._cookie_separator}{guest.access_token};"}, + ) + channel = self.env["discuss.channel"].browse(channel_info["id"]) + channel_guest = channel.channel_member_ids.filtered(lambda member: member.guest_id).guest_id + self.assertNotEqual(channel_guest, guest) + + def test_access_routes_with_valid_guest_token(self): + channel_info = self.make_jsonrpc_request( + "/im_livechat/cors/get_session", + { + "anonymous_name": "Visitor", + "channel_id": self.livechat_channel.id, + "persisted": True, + }, + ) + self.authenticate(None, None) + self.make_jsonrpc_request( + "/im_livechat/cors/channel/messages", + { + "guest_token": channel_info["guest_token"], + "channel_id": channel_info["id"], + }, + ) + + def test_access_denied_for_wrong_channel(self): + channel_info = self.make_jsonrpc_request( + "/im_livechat/cors/get_session", + { + "anonymous_name": "Visitor", + "channel_id": self.livechat_channel.id, + "persisted": True, + }, + ) + guest = self.env["mail.guest"].create({"name": "Visitor"}) + self.authenticate(None, None) + with self.assertRaises(JsonRpcException, msg="werkzeug.exceptions.NotFound"): + self.make_jsonrpc_request( + "/im_livechat/cors/channel/messages", + { + "guest_token": guest.access_token, + "channel_id": channel_info["id"], + }, + ) diff --git a/addons/im_livechat/tools/__init__.py b/addons/im_livechat/tools/__init__.py new file mode 100644 index 00000000000..1d0b8cd77e7 --- /dev/null +++ b/addons/im_livechat/tools/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import misc diff --git a/addons/im_livechat/tools/misc.py b/addons/im_livechat/tools/misc.py new file mode 100644 index 00000000000..a85ced40f3d --- /dev/null +++ b/addons/im_livechat/tools/misc.py @@ -0,0 +1,25 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import request +from werkzeug.exceptions import NotFound + +def downgrade_to_public_user(): + """Replace the request user by the public one. All the cookies are removed + in order to ensure that the no user-specific data is kept in the request.""" + public_user = request.env.ref("base.public_user") + request.update_env(user=public_user) + request.httprequest.cookies = {} + + +def force_guest_env(guest_token): + """Retrieve the guest from the given token and add it to the context. + The request user is then replaced by the public one. + + :param str guest_token: + :raise NotFound: if the guest cannot be found from the token + """ + guest = request.env["mail.guest"]._get_guest_from_token(guest_token) + if not guest: + raise NotFound() + request.update_context(guest=guest) + downgrade_to_public_user() diff --git a/addons/mail/models/discuss/mail_guest.py b/addons/mail/models/discuss/mail_guest.py index 8afdf21c207..4a3274b2957 100644 --- a/addons/mail/models/discuss/mail_guest.py +++ b/addons/mail/models/discuss/mail_guest.py @@ -28,21 +28,15 @@ def add_guest_to_context(func): req.httprequest.cookies.get(req.env["mail.guest"]._cookie_name) or req.env.context.get("guest_token", "") ) - parts = token.split(req.env["mail.guest"]._cookie_separator) - guest = req.env["mail.guest"] - if len(parts) == 2: - guest_id, guest_access_token = parts - guest = req.env["mail.guest"].browse(int(guest_id)).sudo().exists() - if not guest or not guest.access_token or not consteq(guest.access_token, guest_access_token): - guest = req.env["mail.guest"] - elif not guest.timezone: - timezone = req.env["mail.guest"]._get_timezone_from_request(req) - if timezone: - guest._update_timezone(timezone) - guest = guest.sudo(False) - req.update_context(guest=guest) - if hasattr(self, "env"): - self.env.context = {**self.env.context, "guest": guest} + guest = req.env["mail.guest"]._get_guest_from_token(token) + if guest and not guest.timezone: + timezone = req.env["mail.guest"]._get_timezone_from_request(req) + if timezone: + guest._update_timezone(timezone) + if guest: + req.update_context(guest=guest) + if hasattr(self, "env"): + self.env.context = {**self.env.context, "guest": guest} return func(self, *args, **kwargs) return wrapper @@ -83,6 +77,17 @@ class MailGuest(models.Model): for guest in self: guest.im_status = res.get(guest.id, 'offline') + def _get_guest_from_token(self, token=""): + """Returns the guest record for the given token, if applicable.""" + guest = self.env["mail.guest"] + parts = token.split(self._cookie_separator) + if len(parts) == 2: + guest_id, guest_access_token = parts + guest = self.browse(int(guest_id)).sudo().exists() + if not guest or not guest.access_token or not consteq(guest.access_token, guest_access_token): + guest = self.env["mail.guest"] + return guest.sudo(False) + def _get_guest_from_context(self): """Returns the current guest record from the context, if applicable.""" guest = self.env.context.get('guest')