[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>
This commit is contained in:
tsm-odoo
2023-10-06 18:18:29 +00:00
parent 2719b2100d
commit a97abef2d2
39 changed files with 369 additions and 97 deletions
+1
View File
@@ -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',
@@ -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
@@ -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)
@@ -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
@@ -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)
@@ -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/<int:channel_id>/attachment/<int:attachment_id>",
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/<int:channel_id>/image/<int:attachment_id>",
"/im_livechat/cors/channel/<int:channel_id>/image/<int:attachment_id>/<int:width>x<int:height>",
],
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)
@@ -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()
@@ -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 (
@@ -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)) {
@@ -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(),
@@ -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;
},
});
@@ -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);
},
});
@@ -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")
@@ -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" });
+1
View File
@@ -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
@@ -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)
@@ -89,7 +89,9 @@
<template id="loader" name="Livechat : Javascript appending the livechat button">
<t t-translation="off">
if (!window.odoo) {
window.odoo = {};
window.odoo = {
csrf_token: "<t t-out="request.csrf_token(None)"/>",
};
}
odoo.__session_info__ = Object.assign(odoo.__session_info__ || {}, {
livechatData: {
+6 -4
View File
@@ -47,7 +47,9 @@ class AttachmentController(http.Controller):
@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):
env = request.env["ir.attachment"]._get_upload_env(thread_model, thread_id)
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(),
@@ -63,11 +65,11 @@ class AttachmentController(http.Controller):
"res_model": "mail.compose.message",
}
)
if env.user.share:
if thread.env.user.share:
# Only generate the access token if absolutely necessary (= not for internal user).
vals["access_token"] = env["ir.attachment"]._generate_access_token()
vals["access_token"] = thread.env["ir.attachment"]._generate_access_token()
try:
attachment = env["ir.attachment"].create(vals)
attachment = thread.env["ir.attachment"].create(vals)
attachment._post_add_create(**kwargs)
attachmentData = attachment._attachment_format()[0]
if attachment.access_token:
@@ -81,6 +81,7 @@ class Channel(models.Model):
uuid = fields.Char('UUID', size=50, default=_generate_random_token, copy=False)
group_public_id = fields.Many2one('res.groups', string='Authorized Group', compute='_compute_group_public_id', readonly=False, store=True)
invitation_url = fields.Char('Invitation URL', compute='_compute_invitation_url')
allow_public_upload = fields.Boolean(default=False)
_sql_constraints = [
('channel_type_not_null', 'CHECK(channel_type IS NOT NULL)', 'The channel type cannot be empty'),
@@ -860,6 +861,7 @@ class Channel(models.Model):
'group_based_subscription': bool(channel.group_ids),
'create_uid': channel.create_uid.id,
'authorizedGroupFullName': channel.group_public_id.full_name,
'allow_public_upload': channel.allow_public_upload,
}
# add last message preview (only used in mobile)
info['last_message_id'] = channel_last_message_ids.get(channel.id, False)
+1 -14
View File
@@ -1,6 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields
from odoo import models, fields
class IrAttachment(models.Model):
@@ -17,19 +17,6 @@ class IrAttachment(models.Model):
return guest
return super()._bus_notification_target()
@api.model
def _get_upload_env(self, thread_model, thread_id):
"""Overriden to allow guests and (portal) users to upload attachments to channels they have
access to. The base method returns the env of the current request, which is not sudo and
relies on access rights. Guests or (portal) users need sudo to upload attachments."""
if thread_model == "discuss.channel":
return (
self.env["discuss.channel.member"]
._get_as_sudo_from_context_or_raise(channel_id=int(thread_id))
.env
)
return super()._get_upload_env(thread_model, thread_id)
def _attachment_format(self):
attachment_format = super()._attachment_format()
for a in attachment_format:
-4
View File
@@ -88,7 +88,3 @@ class IrAttachment(models.Model):
'model': attachment.res_model,
})],
} for attachment in self]
@api.model
def _get_upload_env(self, thread_model, thread_id):
return request.env
+5 -1
View File
@@ -22,6 +22,7 @@ from email.message import EmailMessage
from email import message_from_string
from lxml import etree
from werkzeug import urls
from werkzeug.exceptions import NotFound
from xmlrpc import client as xmlrpclib
from markupsafe import Markup, escape
@@ -480,7 +481,10 @@ class MailThread(models.AbstractModel):
@api.model
def _get_from_context_or_raise(self, thread_id):
return self.browse(thread_id).exists()
thread = self.search([["id", "=", thread_id]])
if not thread:
raise NotFound()
return thread
# ------------------------------------------------------
# TRACKING / LOG
@@ -121,6 +121,10 @@ export class AttachmentUploadService {
);
}
get uploadURL() {
return "/mail/attachment/upload";
}
async unlink(attachment) {
const abort = this.abortByAttachmentId.get(attachment.id);
const def = this.deferredByAttachmentId.get(attachment.id);
@@ -138,7 +142,7 @@ export class AttachmentUploadService {
this.hookersByTmpId.set(tmpId, hooker);
this.uploadingAttachmentIds.add(tmpId);
await this.fileUploadService
.upload("/mail/attachment/upload", [file], {
.upload(this.uploadURL, [file], {
buildFormData: (formData) => {
this._makeFormData(formData, file, hooker, tmpId, options);
},
+11 -8
View File
@@ -81,12 +81,10 @@ export class Composer extends Component {
_t("<samp>%(send_keybind)s</samp><i> to send</i>", { send_keybind: this.sendKeybind })
);
this.store = useState(useService("mail.store"));
if (this.allowUpload) {
this.attachmentUploader = useAttachmentUploader(
this.thread ?? this.props.composer.message.originThread,
{ composer: this.props.composer }
);
}
this.attachmentUploader = useAttachmentUploader(
this.thread ?? this.props.composer.message.originThread,
{ composer: this.props.composer }
);
this.messageService = useState(useService("mail.message"));
this.threadService = useService("mail.thread");
this.ui = useState(useService("ui"));
@@ -117,8 +115,13 @@ export class Composer extends Component {
this.suggestion = this.store.user ? useSuggestion() : undefined;
this.markEventHandled = markEventHandled;
this.onDropFile = this.onDropFile.bind(this);
if (this.props.dropzoneRef && this.allowUpload) {
useDropzone(this.props.dropzoneRef, this.onDropFile, "o-mail-Composer-dropzone");
if (this.props.dropzoneRef) {
useDropzone(
this.props.dropzoneRef,
this.onDropFile,
"o-mail-Composer-dropzone",
() => this.allowUpload
);
}
if (this.props.messageEdition) {
this.props.messageEdition.composerOfThread = this;
@@ -9,7 +9,7 @@ import { registry } from "@web/core/registry";
const componentRegistry = registry.category("main_components");
let id = 1;
export function useDropzone(targetRef, onDrop, extraClass) {
export function useDropzone(targetRef, onDrop, extraClass, isDropzoneEnabled = () => true) {
const dropzoneId = `mail.dropzone_${id++}`;
let dragCount = 0;
let hasTarget = false;
@@ -26,7 +26,7 @@ export function useDropzone(targetRef, onDrop, extraClass) {
});
function updateDropzone() {
const shouldDisplayDropzone = dragCount && hasTarget;
const shouldDisplayDropzone = dragCount && hasTarget && isDropzoneEnabled();
const hasDropzone = componentRegistry.contains(dropzoneId);
if (shouldDisplayDropzone && !hasDropzone) {
componentRegistry.add(dropzoneId, {
@@ -6,7 +6,7 @@ import { AttachmentList } from "@mail/core/common/attachment_list";
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { useVisible } from "@mail/utils/common/hooks";
import { useSequential, useVisible } from "@mail/utils/common/hooks";
/**
* @typedef {Object} Props
@@ -18,6 +18,9 @@ export class AttachmentPanel extends Component {
static template = "mail.AttachmentPanel";
setup() {
this.sequential = useSequential();
this.store = useService("mail.store");
this.ormService = useService("orm");
this.threadService = useService("mail.thread");
this.attachmentUploadService = useService("mail.attachment_upload");
onWillStart(() => {
@@ -47,4 +50,20 @@ export class AttachmentPanel extends Component {
}
return attachmentsByDate;
}
get hasToggleAllowPublicUpload() {
return (
this.props.thread.model !== "mail.box" &&
this.props.thread.type !== "chat" &&
this.store.user?.user?.isInternalUser
);
}
toggleAllowPublicUpload() {
this.sequential(() =>
this.ormService.write("discuss.channel", [this.props.thread.id], {
allow_public_upload: !this.props.thread.allow_public_upload,
})
);
}
}
@@ -4,6 +4,13 @@
<t t-name="mail.AttachmentPanel">
<t t-set="title">Attachments</t>
<ActionPanel title="title">
<div t-if="hasToggleAllowPublicUpload" class="form-check form-switch">
<label class="form-check-label">
<input t-on-change="toggleAllowPublicUpload" class="form-check-input" type="checkbox" role="switch" t-att-checked="props.thread.allow_public_upload"/>
<t t-if="props.thread.allow_public_upload">File upload is enabled for external users</t>
<t t-else="">File upload is disabled for external users</t>
</label>
</div>
<div class="flex-grow-1" t-att-class="{
'd-flex justify-content-center align-items-center': props.thread.attachments.length === 0,
}">
@@ -0,0 +1,24 @@
/* @odoo-module */
import { AttachmentUploadService } from "@mail/core/common/attachment_upload_service";
import { patch } from "@web/core/utils/patch";
patch(AttachmentUploadService.prototype, {
setup() {
super.setup(...arguments);
this.env.services["bus_service"].subscribe("mail.record/insert", ({ Thread }) => {
if (
Thread &&
"allow_public_upload" in Thread &&
!Thread.allow_public_upload &&
!this.store.user?.user?.isInternalUser
) {
const attachments = [...this.store.Thread.insert(Thread).composer.attachments];
for (const attachment of attachments) {
this.unlink(attachment);
}
}
});
},
});
@@ -0,0 +1,16 @@
/* @odoo-module */
import { Composer } from "@mail/core/common/composer";
import { patch } from "@web/core/utils/patch";
patch(Composer.prototype, {
get allowUpload() {
const thread = this.thread ?? this.message.originThread;
return (
super.allowUpload &&
(thread.model !== "discuss.channel" ||
thread?.allow_public_upload ||
this.store.user?.user?.isInternalUser)
);
},
});
@@ -19,4 +19,8 @@ patch(Thread.prototype, {
}
return super.imgUrl;
},
update(data) {
super.update(data);
assignDefined(this, data, ["allow_public_upload"]);
},
});
@@ -49,3 +49,23 @@ QUnit.test("Attachment panel sort by date", async () => {
after: [".o-mail-DateSection", { text: "August, 2023" }],
});
});
QUnit.test("Can toggle allow public upload", async () => {
const pyEnv = await startServer();
const channelId = await pyEnv["discuss.channel"].create({ name: "General" });
const tab1 = await start({ asTab: true });
await tab1.openDiscuss(channelId);
await click(".o-mail-Discuss-header button[title='Show Attachments']", { target: tab1.target });
const tab2 = await start({ asTab: true });
await tab2.openDiscuss(channelId);
await click(".o-mail-Discuss-header button[title='Show Attachments']", { target: tab2.target });
await contains(".o-mail-ActionPanel", {
contains: ["label", { text: "File upload is disabled for external users" }],
target: tab2.target,
});
await click(".o-mail-ActionPanel input[type='checkbox']", { target: tab1.target });
await contains(".o-mail-ActionPanel", {
contains: ["label", { text: "File upload is enabled for external users" }],
target: tab2.target,
});
});
@@ -735,6 +735,7 @@ patch(MockServer.prototype, {
],
];
res.channel = channelData;
res.allow_public_upload = channel.allow_public_upload;
return res;
});
},
@@ -220,7 +220,7 @@ patch(MockServer.prototype, {
type: "partner",
};
if (user) {
author["user"] = { id: user.id };
author["user"] = { id: user.id, isInternalUser: !user.share };
}
} else {
author = false;
@@ -279,6 +279,7 @@ patch(MockServer.prototype, {
user: mainUser
? {
id: mainUser.id,
isInternalUser: !mainUser.share,
}
: false,
},
+1
View File
@@ -4,5 +4,6 @@ from . import test_discuss_channel
from . import test_discuss_channel_as_guest
from . import test_discuss_channel_member
from . import test_message_controller
from . import test_toggle_upload
from . import test_load_messages
from . import test_rtc
@@ -29,12 +29,14 @@ class TestMailPublicPage(HttpCase):
guest = self.env['mail.guest'].create({'name': 'Guest Mario'})
self.channel = self.env['discuss.channel'].browse(self.env['discuss.channel'].channel_create(group_id=None, name='Test channel')['id'])
self.channel.allow_public_upload = True
self.channel.add_members(portal_user.partner_id.ids)
self.channel.add_members(internal_user.partner_id.ids)
self.channel.add_members(guest_ids=[guest.id])
self.group = self.env['discuss.channel'].browse(self.env['discuss.channel'].create_group(partners_to=(internal_user + portal_user).partner_id.ids, name="Test group")['id'])
self.group.add_members(guest_ids=[guest.id])
self.group.allow_public_upload = True
self.tour = "mail/static/tests/tours/discuss_public_tour.js"
@@ -0,0 +1,51 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from requests.exceptions import HTTPError
from odoo import Command, http
from odoo.tests import tagged, HttpCase
from odoo.tools import file_open, mute_logger
@tagged("post_install", "-at_install")
class TestToggleUpload(HttpCase):
def test_upload_allowed(self):
self.authenticate(None, None)
channel = self.env["discuss.channel"].create({"name": "General"})
guest = self.env["mail.guest"].create({"name": "Guest"})
channel.write({"channel_member_ids": [Command.create({"guest_id": guest.id})]})
self.assertFalse(channel.allow_public_upload)
channel.write({'allow_public_upload': True})
with 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.id,
"thread_model": "discuss.channel",
},
files={"ufile": file},
headers={"Cookie": f"session_id={self.session.sid};{guest._cookie_name}={guest._format_auth_cookie()};"},
)
self.assertEqual(response.status_code, 200)
def test_upload_denied(self):
self.authenticate(None, None)
channel = self.env["discuss.channel"].create({"name": "General"})
guest = self.env["mail.guest"].create({"name": "Guest"})
channel.write({"channel_member_ids": [Command.create({"guest_id": guest.id})]})
self.assertFalse(channel.allow_public_upload)
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.id,
"thread_model": "discuss.channel",
},
files={"ufile": file},
headers={"Cookie": f"session_id={self.session.sid};{guest._cookie_name}={guest._format_auth_cookie()};"},
)
with self.assertRaises(HTTPError) as error_catcher:
response.raise_for_status()
self.assertEqual(error_catcher.exception.response.status_code, 403)
@@ -131,6 +131,7 @@ class TestDiscussFullPerformance(HttpCase):
'odoobotOnboarding': False,
'channels': [
{
'allow_public_upload': False,
'authorizedGroupFullName': self.group_user.full_name,
'channel': {
'anonymous_country': False,
@@ -181,6 +182,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_general.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': False,
'channel': {
'anonymous_country': False,
@@ -231,6 +233,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_channel_public_1.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': False,
'channel': {
'anonymous_country': False,
@@ -281,6 +284,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_channel_public_2.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': self.group_user.full_name,
'channel': {
'anonymous_country': False,
@@ -331,6 +335,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_channel_group_1.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': self.group_user.full_name,
'channel': {
'anonymous_country': False,
@@ -381,6 +386,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_channel_group_2.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': False,
'channel': {
'anonymous_country': False,
@@ -467,6 +473,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_group_1.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': False,
'channel': {
'anonymous_country': False,
@@ -553,6 +560,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_chat_1.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': False,
'channel': {
'anonymous_country': False,
@@ -639,6 +647,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_chat_2.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': False,
'channel': {
'anonymous_country': False,
@@ -725,6 +734,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_chat_3.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': False,
'channel': {
'anonymous_country': False,
@@ -811,6 +821,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_chat_4.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': False,
'channel': {
'anonymous_country': {
@@ -896,6 +907,7 @@ class TestDiscussFullPerformance(HttpCase):
'uuid': self.channel_livechat_1.uuid,
},
{
'allow_public_upload': False,
'authorizedGroupFullName': False,
'channel': {
'anonymous_country': {
@@ -23,6 +23,7 @@ export class FileUploader extends Component {
if (!ev.target.files.length) {
return;
}
const { target } = ev;
for (const file of ev.target.files) {
if (!checkFileSize(file.size, this.notification)) {
return null;
@@ -47,7 +48,7 @@ export class FileUploader extends Component {
this.state.isUploading = false;
}
}
ev.target.value = null;
target.value = null;
if (this.props.multiUpload && this.props.onUploadComplete) {
this.props.onUploadComplete({});
}