From 6494f511718893eec3573c60c0a62e04d386359d Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 7 Feb 2018 14:11:46 +0100 Subject: [PATCH] [FIX] base/mail: Added an access_token on the attachments url The access_token is automatically added by the media editor. It's added on the website and by the html editor to send by email. The widget display the attachments from 'ir.ui.view' (public attachment) and the attachment from the current record. Issue: can't load image in gmail or mobile because the client mail use a proxy who avoid ours odoo access. --- addons/mail/models/mail_message.py | 5 +- addons/mail/tests/test_mail_message.py | 9 ++ addons/web_editor/controllers/main.py | 15 +- .../static/src/js/backend/fields.js | 2 + .../static/src/js/editor/rte.summernote.js | 6 +- .../static/src/js/editor/snippets.options.js | 7 +- .../static/src/js/widgets/widgets.js | 131 +++++++++++------- addons/web_editor/static/src/xml/editor.xml | 7 +- odoo/addons/base/ir/ir_attachment.py | 11 +- odoo/addons/base/ir/ir_http.py | 7 +- 10 files changed, 138 insertions(+), 62 deletions(-) diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 9fa6701be16..45d07d5b97c 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -757,9 +757,10 @@ class Message(models.Model): 'datas_fname': name, 'res_model': 'mail.message', }) + attachment.generate_access_token() values['attachment_ids'].append((4, attachment.id)) - data_to_url[key] = '/web/image/%s' % attachment.id - return '%s%s alt="%s"' % (data_to_url[key], match.group(3), name) + data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name] + return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1]) values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body'])) # delegate creation of tracking after the create as sudo to avoid access rights issues diff --git a/addons/mail/tests/test_mail_message.py b/addons/mail/tests/test_mail_message.py index 64b4c16ecbd..c91cc373ced 100644 --- a/addons/mail/tests/test_mail_message.py +++ b/addons/mail/tests/test_mail_message.py @@ -113,6 +113,15 @@ class TestMailMessage(TestMail): self.assertNotIn(u'Ernest Employee ', self.email_to_list) self.assertIn(u'test@example.com', self.email_to_list) + def test_mail_message_base64_image(self): + msg = self.env['mail.message'].sudo(self.user_employee).create({ + 'body': 'taratata ', + }) + self.assertEqual(len(msg.attachment_ids), 1) + body = '

taratata image0 image0

' + body = body % (msg.attachment_ids[0].id, msg.attachment_ids[0].access_token, msg.attachment_ids[0].id, msg.attachment_ids[0].access_token) + self.assertEqual(msg.body, body) + class TestMailMessageAccess(TestMail): diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 50a9540323a..cc8d91cbaef 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -146,6 +146,9 @@ class Web_Editor(http.Controller): # therefore we have to recover the files from the request object Attachments = request.env['ir.attachment'] # registry for the attachment table + res_model = kwargs.get('res_model', 'ir.ui.view') + res_id = res_model != 'ir.ui.view' and kwargs.get('res_id') or None + uploads = [] message = None if not upload: # no image provided, storing the link and the image name @@ -155,9 +158,11 @@ class Web_Editor(http.Controller): 'type': 'url', 'url': url, 'public': True, - 'res_model': 'ir.ui.view', + 'res_id': res_id, + 'res_model': res_model, }) - uploads += attachment.read(['name', 'mimetype', 'checksum', 'url']) + attachment.generate_access_token() + uploads += attachment.read(['name', 'mimetype', 'checksum', 'url', 'res_id', 'res_model', 'access_token']) else: # images provided try: attachments = request.env['ir.attachment'] @@ -180,10 +185,12 @@ class Web_Editor(http.Controller): 'datas': base64.b64encode(data), 'datas_fname': c_file.filename, 'public': True, - 'res_model': 'ir.ui.view', + 'res_id': res_id, + 'res_model': res_model, }) + attachment.generate_access_token() attachments += attachment - uploads += attachments.read(['name', 'mimetype', 'checksum', 'url']) + uploads += attachments.read(['name', 'mimetype', 'checksum', 'url', 'res_id', 'res_model', 'access_token']) except Exception as e: logger.exception("Failed to upload image to attachment") message = pycompat.text_type(e) diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js index b84fcd21c91..ae7ee94e964 100644 --- a/addons/web_editor/static/src/js/backend/fields.js +++ b/addons/web_editor/static/src/js/backend/fields.js @@ -133,6 +133,8 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi this.$textarea.summernote(this._getSummernoteConfig()); this.$content = this.$('.note-editable:first'); this.$content.html(this._textToHtml(this.value)); + this.$content.data('oe-id', this.recordData.res_id || this.res_id); + this.$content.data('oe-model', this.recordData.model || this.model); // trigger a mouseup to refresh the editor toolbar this.$content.trigger('mouseup'); if (this.nodeOptions['style-inline']) { diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js index 2ac9df2d693..71982f8137b 100644 --- a/addons/web_editor/static/src/js/editor/rte.summernote.js +++ b/addons/web_editor/static/src/js/editor/rte.summernote.js @@ -1080,8 +1080,12 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, { return; } data.__alreadyDone = true; + var mediaDialog = new weWidgets.MediaDialog(this, - data.options || {}, + _.extend({ + res_model: data.$editable.data('oe-model'), + res_id: data.$editable.data('oe-id'), + }, data.options), data.$editable, data.media ); diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 38dcacf9e77..382bae0d4a7 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -872,7 +872,12 @@ registry.background = SnippetOption.extend({ // Put fake image in the DOM, edit it and use it as background-image var $image = $('', {class: 'hidden', src: value}).appendTo(this.$target); - var _editor = new widget.MediaDialog(this, {}, null, $image[0]).open(); + var $editable = this.$target.closest('.o_editable'); + var options = { + res_model: $editable.data('oe-model'), + res_id: $editable.data('oe-id'), + }; + var _editor = new widget.MediaDialog(this, options, null, $image[0]).open(); _editor.opened(function () { _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); }); diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js index 1f150b16862..b93724c7a30 100644 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -120,7 +120,13 @@ var ImageDialog = Widget.extend({ this._super.apply(this, arguments); this.options = options || {}; this.accept = this.options.accept || this.options.document ? "*/*" : "image/*"; - this.domain = this.options.domain || ['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]]; + if (this.options.res_id) { + this.domain = ['|', + '&', ['res_model', '=', this.options.res_model], ['res_id', '=', this.options.res_id], + ['res_model', '=', 'ir.ui.view']]; + } else { + this.domain = [['res_model', '=', 'ir.ui.view']]; + } this.parent = parent; this.old_media = media; this.media = media; @@ -171,6 +177,7 @@ var ImageDialog = Widget.extend({ } }, save: function () { + var self = this; if (this.options.select_images) { return this.images; } @@ -181,34 +188,55 @@ var ImageDialog = Widget.extend({ img = _.find(this.images, function (img) { return img.id === id;}); } - var media; - if (!img.is_document) { - if (this.media.tagName !== "IMG" || !this.old_media) { - this.add_class = "pull-left"; - this.style = {"width": "100%"}; - } - if (this.media.tagName !== "IMG") { - media = document.createElement('img'); - $(this.media).replaceWith(media); - this.media = media; - } - this.media.setAttribute('src', img.src); - } else { - if (this.media.tagName !== "A") { - $('.note-control-selection').hide(); - media = document.createElement('a'); - $(this.media).replaceWith(media); - this.media = media; - } - this.media.setAttribute('href', '/web/content/' + img.id + '?unique=' + img.checksum + '&download=true'); - $(this.media).addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype); + var def = $.when(); + if (!img.access_token) { + def = this._rpc({ + model: 'ir.attachment', + method: 'generate_access_token', + args: [[img.id]] + }).then(function (access_token) { + img.access_token = access_token[0]; + }); } - $(this.media).attr('alt', img.alt); - var style = this.style; - if (style) { $(this.media).css(style); } + return def.then(function () { + var media; + if (!img.is_document) { + if (img.access_token && self.options.res_model !== 'ir.ui.view') { + img.src += _.str.sprintf('?access_token=%s', img.access_token); + } + if (self.media.tagName !== "IMG" || !self.old_media) { + self.add_class = "pull-left"; + self.style = {"width": "100%"}; + } + if (self.media.tagName !== "IMG") { + media = document.createElement('img'); + $(self.media).replaceWith(media); + self.media = media; + } + self.media.setAttribute('src', img.src); + } else { + if (self.media.tagName !== "A") { + $('.note-control-selection').hide(); + media = document.createElement('a'); + $(self.media).replaceWith(media); + self.media = media; + } + var href = '/web/content/' + img.id + '?'; + if (img.access_token && self.options.res_model !== 'ir.ui.view') { + href += _.str.sprintf('access_token=%s&', img.access_token); + } + href += 'unique=' + img.checksum + '&download=true'; + self.media.setAttribute('href', href); + $(self.media).addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype); + } - return this.media; + $(self.media).attr('alt', img.alt); + var style = self.style; + if (style) { $(self.media).css(style); } + + return self.media; + }); }, clear: function () { this.media.className = this.media.className.replace(/(^|\s+)((img(\s|$)|img-(?!circle|rounded|thumbnail))[^\s]*)/g, ' '); @@ -282,7 +310,7 @@ var ImageDialog = Widget.extend({ } }, fetch_existing: function (needle) { - var domain = [['res_model', '=', 'ir.ui.view']].concat(this.domain); + var domain = this.domain.concat(['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]]); if (needle && needle.length) { domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]); } @@ -292,7 +320,7 @@ var ImageDialog = Widget.extend({ args: [], kwargs: { domain: domain, - fields: ['name', 'mimetype', 'checksum', 'url', 'type'], + fields: ['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'access_token'], order: [{name: 'id', asc: false}], context: weContext.get(), } @@ -1001,18 +1029,21 @@ var MediaDialog = Dialog.extend({ return this._super.apply(this, arguments); }, save: function () { + var self = this; + var args = arguments; + var _super = this._super; if (this.options.select_images) { - this.final_data = this.active.save(); - this._super.apply(this, arguments); - return; + return $.when(this.active.save()).then(function (data) { + self.final_data = data; + return _super.apply(self, args); + }); } if (this.rte) { this.range.select(); this.rte.historyRecordUndo(this.media); } - var self = this; - if (self.media) { + if (this.media) { this.media.innerHTML = ""; if (this.active !== this.imageDialog && this.active !== this.documentDialog) { this.imageDialog.clear(); @@ -1029,27 +1060,27 @@ var MediaDialog = Dialog.extend({ this.range.insertNode(this.media, true); this.active.media = this.media; } - this.active.save(); - if (this.active.add_class) { - $(this.active.media).addClass(this.active.add_class); - } - var media = this.active.media; + return $.when(this.active.save()).then(function () { + if (self.active.add_class) { + $(self.active.media).addClass(self.active.add_class); + } + var media = self.active.media; - this.final_data = [media, self.old_media]; - $(document.body).trigger("media-saved", this.final_data); - $(self.old_media).trigger("save", this.final_data); - $(this.final_data).trigger('input'); + self.final_data = [media, self.old_media]; + $(document.body).trigger("media-saved", self.final_data); + $(self.old_media).trigger("save", self.final_data); + $(self.final_data).trigger('input'); - // Update editor bar after image edition (in case the image change to icon or other) - _.defer(function () { - if (!media.parentNode) return; - range.createFromNode(media).select(); - click_event(media, "mousedown"); - click_event(media, "mouseup"); + // Update editor bar after image edition (in case the image change to icon or other) + _.defer(function () { + if (!media.parentNode) return; + range.createFromNode(media).select(); + click_event(media, "mousedown"); + click_event(media, "mouseup"); + }); + return _super.apply(self, args); }); - - this._super.apply(this, arguments); }, searchTimer: null, search: function () { diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index e8e6374305e..8f975b0d784 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -86,6 +86,8 @@ target="fileframe" class="form-inline"> + +
@@ -145,8 +147,9 @@
- -
+ + +
diff --git a/odoo/addons/base/ir/ir_attachment.py b/odoo/addons/base/ir/ir_attachment.py index 19409a3b78b..bae99e919a6 100644 --- a/odoo/addons/base/ir/ir_attachment.py +++ b/odoo/addons/base/ir/ir_attachment.py @@ -8,6 +8,7 @@ import mimetypes import os import re from collections import defaultdict +import uuid from odoo import api, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import AccessError @@ -280,7 +281,7 @@ class IrAttachment(models.Model): public = fields.Boolean('Is public document') # for external access - access_token = fields.Char('Access Token') + access_token = fields.Char('Access Token', groups="base.group_user") # the field 'datas' is computed and may use the other fields below datas = fields.Binary(string='File Content', compute='_compute_datas', inverse='_inverse_datas') @@ -438,6 +439,14 @@ class IrAttachment(models.Model): self.browse().check('write', values=values) return super(IrAttachment, self).create(values) + @api.one + def generate_access_token(self): + if self.access_token: + return self.access_token + access_token = str(uuid.uuid4()) + self.write({'access_token': access_token}) + return access_token + @api.model def action_get(self): return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment') diff --git a/odoo/addons/base/ir/ir_http.py b/odoo/addons/base/ir/ir_http.py index 27d759e5461..751f4aed9ee 100644 --- a/odoo/addons/base/ir/ir_http.py +++ b/odoo/addons/base/ir/ir_http.py @@ -21,7 +21,7 @@ import odoo from odoo import api, http, models, tools, SUPERUSER_ID from odoo.exceptions import AccessDenied, AccessError from odoo.http import request, STATIC_CACHE, content_disposition -from odoo.tools import pycompat +from odoo.tools import pycompat, consteq from odoo.tools.mimetypes import guess_mimetype from odoo.modules.module import get_resource_path, get_module_path @@ -269,6 +269,7 @@ class IrHttp(models.AbstractModel): :param str mimetype: mintype of the field (for headers) :param str default_mimetype: default mintype if no mintype found :param str access_token: optional token for unauthenticated access + only available for ir.attachment :param Environment env: by default use request.env :returns: (status, headers, content) """ @@ -277,6 +278,10 @@ class IrHttp(models.AbstractModel): obj = None if xmlid: obj = env.ref(xmlid, False) + elif id and model == 'ir.attachment' and access_token: + obj = env[model].sudo().browse(int(id)) + if not consteq(obj.access_token, access_token): + return (403, [], None) elif id and model in env.registry: obj = env[model].browse(int(id))