[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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -113,6 +113,15 @@ class TestMailMessage(TestMail):
|
||||
self.assertNotIn(u'Ernest Employee <e.e@example.com>', 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 <img src="data:image/png;base64,iV/+OkI=" width="2"> <img src="data:image/png;base64,iV/+OkI=" width="2">',
|
||||
})
|
||||
self.assertEqual(len(msg.attachment_ids), 1)
|
||||
body = '<p>taratata <img src="/web/image/%s?access_token=%s" alt="image0" width="2"> <img src="/web/image/%s?access_token=%s" alt="image0" width="2"></p>'
|
||||
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):
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -872,7 +872,12 @@ registry.background = SnippetOption.extend({
|
||||
// Put fake image in the DOM, edit it and use it as background-image
|
||||
var $image = $('<img/>', {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');
|
||||
});
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
target="fileframe"
|
||||
class="form-inline">
|
||||
<input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
|
||||
<input t-if="widget.options.res_id" type="hidden" name="res_id" t-att-value="widget.options.res_id"/>
|
||||
<input t-if="widget.options.res_model" type="hidden" name="res_model" t-att-value="widget.options.res_model"/>
|
||||
<div class="well">
|
||||
<div class="form-group pull-left">
|
||||
<input type="file" name="upload" t-att-accept="widget.accept" multiple="multiple" style="position: absolute; opacity: 0; width: 1px; height: 1px;"/>
|
||||
@@ -145,8 +147,9 @@
|
||||
<div class="existing-attachments">
|
||||
<div class="row mt16" t-as="row" t-foreach="rows">
|
||||
<div class="col-sm-2 o_existing_attachment_cell" t-as="attachment" t-foreach="row">
|
||||
<i class="fa fa-times o_existing_attachment_remove" t-att-data-id="attachment.id"/>
|
||||
<div class="o_attachment_border"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" class="o_image"/></div>
|
||||
<i t-if="attachment.res_model === 'ir.ui.view'" class="fa fa-times o_existing_attachment_remove" title="This file is a public view attachment" t-att-data-id="attachment.id"/>
|
||||
<i t-else="" class="fa fa-times o_existing_attachment_remove" title="This file is attached to the current record" t-att-data-id="attachment.id"/>
|
||||
<div class="o_attachment_border" t-att-style="attachment.res_model === 'ir.ui.view' ? null : 'border: 1px solid #5cb85c;'"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" class="o_image"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user