[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:
Christophe Matthieu
2018-02-08 11:25:51 +01:00
parent 152e3f8686
commit 6494f51171
10 changed files with 138 additions and 62 deletions
+3 -2
View File
@@ -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
+9
View File
@@ -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):
+11 -4
View File
@@ -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 () {
+5 -2
View File
@@ -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>
+10 -1
View File
@@ -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')
+6 -1
View File
@@ -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))