diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 7f1769050a3..b4e03d97de5 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -292,6 +292,8 @@ class Web_Editor(http.Controller): def _attachment_create(self, name='', data=False, url=False, res_id=False, res_model='ir.ui.view'): """Create and return a new attachment.""" + IrAttachment = request.env['ir.attachment'] + if name.lower().endswith('.bmp'): # Avoid mismatch between content type and mimetype, see commit msg name = name[:-4] @@ -323,15 +325,13 @@ class Web_Editor(http.Controller): else: raise UserError(_("You need to specify either data or url to create an attachment.")) - # If the user is a portal and has access rights on the res_model (and - # on the res_id if it exists), we can create the attachment. Otherwise, - # one of the check will raise an error and the attachment will not be - # created - if request.env.user.has_group('base.group_portal'): - request.env[res_model].check_access_rights('write') - if res_id: - request.env[res_model].browse(res_id).check_access_rule('write') - attachment = request.env['ir.attachment'].sudo().create(attachment_data) + # Despite the user having no right to create an attachment, he can still + # create an image attachment through some flows + if ( + not request.env.is_admin() + and IrAttachment._can_bypass_rights_on_media_dialog(**attachment_data) + ): + attachment = IrAttachment.sudo().create(attachment_data) # When portal users upload an attachment with the wysiwyg widget, # the access token is needed to use the image in the editor. If # the attachment is not public, the user won't be able to generate @@ -339,7 +339,7 @@ class Web_Editor(http.Controller): if not attachment_data['public']: attachment.sudo().generate_access_token() else: - attachment = request.env['ir.attachment'].create(attachment_data) + attachment = IrAttachment.create(attachment_data) return attachment diff --git a/addons/web_editor/models/ir_attachment.py b/addons/web_editor/models/ir_attachment.py index 6eede12910c..abc192f0978 100644 --- a/addons/web_editor/models/ir_attachment.py +++ b/addons/web_editor/models/ir_attachment.py @@ -63,3 +63,12 @@ class IrAttachment(models.Model): """Return a dict with the values that we need on the media dialog.""" self.ensure_one() return self._read_format(['id', 'name', 'description', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height', 'original_id'])[0] + + def _can_bypass_rights_on_media_dialog(self, **attachment_data): + """ This method is meant to be overridden, for instance to allow to + create image attachment despite the user not allowed to create + attachment, eg: + - Portal user uploading an image on the forum (bypass acl) + - Non admin user uploading an unsplash image (bypass binary/url check) + """ + return False diff --git a/addons/web_unsplash/models/__init__.py b/addons/web_unsplash/models/__init__.py index 06c3c1ac4cd..d69af5fe79a 100644 --- a/addons/web_unsplash/models/__init__.py +++ b/addons/web_unsplash/models/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import ir_attachment +from . import ir_qweb_fields from . import res_config_settings from . import res_users -from . import ir_qweb_fields diff --git a/addons/web_unsplash/models/ir_attachment.py b/addons/web_unsplash/models/ir_attachment.py new file mode 100644 index 00000000000..373d0d933a9 --- /dev/null +++ b/addons/web_unsplash/models/ir_attachment.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Attachment(models.Model): + + _inherit = "ir.attachment" + + def _can_bypass_rights_on_media_dialog(self, **attachment_data): + # We need to allow and sudo the case of an "url + file" attachment, + # which is by default forbidden for non admin. + # See `_check_serving_attachments` + forbidden = 'url' in attachment_data and attachment_data.get('type', 'binary') == 'binary' + if forbidden and attachment_data['url'].startswith('/unsplash/'): + return True + return super()._can_bypass_rights_on_media_dialog(**attachment_data) diff --git a/addons/website_forum/models/__init__.py b/addons/website_forum/models/__init__.py index 6400bd0bdb4..cb7e2438978 100644 --- a/addons/website_forum/models/__init__.py +++ b/addons/website_forum/models/__init__.py @@ -2,5 +2,6 @@ from . import forum from . import gamification +from . import ir_attachment from . import res_users from . import website diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index e3fab92f903..b071a43f4d6 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -374,6 +374,9 @@ class Post(models.Model): can_post = fields.Boolean('Can Automatically be Validated', compute='_get_post_karma_rights', compute_sudo=False) can_flag = fields.Boolean('Can Flag', compute='_get_post_karma_rights', compute_sudo=False) can_moderate = fields.Boolean('Can Moderate', compute='_get_post_karma_rights', compute_sudo=False) + can_use_full_editor = fields.Boolean( + compute='_get_post_karma_rights', compute_sudo=False, + help="Editor Features: image and links") def _search_can_view(self, operator, value): if operator not in ('=', '!=', '<>'): @@ -495,6 +498,7 @@ class Post(models.Model): post.can_post = is_admin or user.karma >= post.forum_id.karma_post post.can_flag = is_admin or user.karma >= post.forum_id.karma_flag post.can_moderate = is_admin or user.karma >= post.forum_id.karma_moderate + post.can_use_full_editor = is_admin or user.karma >= post.forum_id.karma_editor def _update_content(self, content, forum_id): forum = self.env['forum.forum'].browse(forum_id) diff --git a/addons/website_forum/models/ir_attachment.py b/addons/website_forum/models/ir_attachment.py new file mode 100644 index 00000000000..e518d9ef740 --- /dev/null +++ b/addons/website_forum/models/ir_attachment.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Attachment(models.Model): + + _inherit = "ir.attachment" + + def _can_bypass_rights_on_media_dialog(self, **attachment_data): + # Bypass the attachment create ACL and let the user create the image + # attachment if he has write right on the model (the image attachment + # will be bound to this model's record). + res_model = attachment_data['res_model'] + res_id = attachment_data.get('res_id') + if ( + res_model == 'forum.post' and res_id + and self.env['forum.post'].browse(res_id).can_use_full_editor + ): + return True + + return super()._can_bypass_rights_on_media_dialog(**attachment_data) diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index 5e06d5a3aca..5dd5bcc3612 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -129,7 +129,11 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ recordInfo: { context: self._getContext(), res_model: 'forum.post', - res_id: +window.location.pathname.split('-').pop(), + // Id is retrieved from URL, which is either: + // - /forum/name-1/post/something-5 + // - /forum/name-1/post/something-5/edit + // TODO: Make this more robust. + res_id: +window.location.pathname.split('-').slice(-1)[0].split('/')[0], }, resizable: true, userGeneratedContent: true, diff --git a/addons/website_forum/tests/test_controller.py b/addons/website_forum/tests/test_controller.py index 6f2fc6c794c..9dfe82ed84d 100644 --- a/addons/website_forum/tests/test_controller.py +++ b/addons/website_forum/tests/test_controller.py @@ -10,10 +10,8 @@ class TestController(HttpCase): @classmethod def setUpClass(cls): super().setUpClass() - portal_user = new_test_user(cls.env, login='portal_user', groups='base.group_portal') - cls.portal = portal_user.login - admin_user = new_test_user(cls.env, login='admin_user', groups='base.group_user,base.group_system') - cls.admin = admin_user.login + cls.portal_user = new_test_user(cls.env, login='portal_user', groups='base.group_portal') + cls.admin_user = new_test_user(cls.env, login='admin_user', groups='base.group_user,base.group_system') cls.headers = {"Content-Type": "application/json"} cls.pixel = 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=' @@ -29,15 +27,16 @@ class TestController(HttpCase): } def test_01_portal_attachment(self): - self.authenticate(self.portal, self.portal) - payload = self._build_payload({"name": "pixel", "data": self.pixel, "is_image": True, "res_model": "forum.post"}) + self.authenticate(self.portal_user.login, self.portal_user.login) + payload = self._build_payload({'name': 'pixel', 'data': self.pixel, 'is_image': True, 'res_model': 'forum.post', 'res_id': 1}) + self.portal_user.karma = 30 response = self.url_open('/web_editor/attachment/add_data', data=json.dumps(payload), headers=self.headers, timeout=60000) self.assertEqual(200, response.status_code) attachment = self.env['ir.attachment'].search([('name', '=', 'pixel')]) self.assertTrue(attachment) def test_02_admin_attachment(self): - self.authenticate(self.admin, self.admin) + self.authenticate(self.admin_user.login, self.admin_user.login) payload = self._build_payload({"name": "pixel", "data": self.pixel, "is_image": True, "res_model": "forum.post"}) response = self.url_open('/web_editor/attachment/add_data', data=json.dumps(payload), headers=self.headers) self.assertEqual(200, response.status_code)