diff --git a/addons/gamification/static/src/js/gamification.js b/addons/gamification/static/src/js/gamification.js index 649df68ecdc3b..1a096d2e68124 100644 --- a/addons/gamification/static/src/js/gamification.js +++ b/addons/gamification/static/src/js/gamification.js @@ -113,7 +113,7 @@ var Sidebar = Widget.extend({ render_user_avatars: function(item) { item.find(".oe_user_avatar").each(function() { var user_id = parseInt( $(this).attr('data-id'), 10); - var url = session.url('/web/binary/image', {model: 'res.users', field: 'image_small', id: user_id}); + var url = session.url('/web/image', {model: 'res.users', field: 'image_small', id: user_id}); $(this).attr("src", url); }); } diff --git a/addons/im_chat/static/src/js/im_chat_backend.js b/addons/im_chat/static/src/js/im_chat_backend.js index 08eb06ab2e186..cd1cc00fdf2dc 100644 --- a/addons/im_chat/static/src/js/im_chat_backend.js +++ b/addons/im_chat/static/src/js/im_chat_backend.js @@ -235,7 +235,7 @@ var InstantMessaging = Widget.extend({ self.widgets = {}; self.users = []; _.each(result, function(user) { - user.image_url = session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: user.id}); + user.image_url = session.url('/web/image', {model:'res.users', field: 'image_small', id: user.id}); var widget = new UserWidget(self, user); widget.appendTo(self.$(".oe_im_users")); widget.on("user_clicked", self, self.user_clicked); diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index a9c6d98eaacb4..b4072560b66a2 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -13,22 +13,6 @@ class MailController(http.Controller): _cp_path = '/mail' - @http.route('/mail/download_attachment', type='http', auth='user') - def download_attachment(self, model, id, method, attachment_id, **kw): - # FIXME use /web/binary/saveas directly - Model = request.registry.get(model) - res = getattr(Model, method)(request.cr, request.uid, int(id), int(attachment_id)) - if res: - filecontent = base64.b64decode(res.get('base64')) - filename = res.get('filename') - content_type = mimetypes.guess_type(filename) - if filecontent and filename: - return request.make_response( - filecontent, - headers=[('Content-Type', content_type[0] or 'application/octet-stream'), - ('Content-Disposition', content_disposition(filename))]) - return request.not_found() - @http.route('/mail/receive', type='json', auth='none') def receive(self, req): """ End-point to receive mail from an external SMTP server. """ diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index ea211c7634969..69a805558c956 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -142,22 +142,6 @@ def _search_starred(self, operator, operand): def _needaction_domain_get(self): return [('needaction', '=', True)] - #------------------------------------------------------ - # download an attachment - #------------------------------------------------------ - - @api.multi - def download_attachment(self, attachment_id): - self.ensure_one() - if attachment_id in self.attachment_ids.ids: - attachment = self.env['ir.attachment'].sudo().browse(attachment_id) - if attachment.datas and attachment.datas_fname: - return { - 'base64': attachment.datas, - 'filename': attachment.datas_fname, - } - return False - #------------------------------------------------------ # Notification API #------------------------------------------------------ diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 3daf47705f6c8..9f3ce11dca607 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -253,7 +253,7 @@ var Attachment = Widget.extend ({ for (var l in message.attachment_ids) { var attach = message.attachment_ids[l]; if (!attach.formating) { - attach.url = mail_utils.get_attachment_url(session, message.id, attach.id); + attach.url = '/web/content/' + attach.id + '?download=true'; attach.name = mail_utils.breakword(attach.name || attach.filename); attach.formating = true; } @@ -264,13 +264,6 @@ var Attachment = Widget.extend ({ display_attachments: function (message) { return QWeb.render('ThreadMessageAttachments', {'record': message, 'widget': this}); }, - - /** - * Return the link to resized image - */ - attachments_resize_image: function (id, resize) { - return mail_utils.get_image(session, 'ir.attachment', 'datas', id, resize); - }, }); /** @@ -907,11 +900,9 @@ var MailThread = Attachment.extend ({ avt = ('/mail/static/src/img/email_icon.png'); } else if (author) { - avt = mail_utils.get_image( - session, 'res.partner', 'image_small', author[0]); + avt = '/web/image/res.partner/' + author[0] + '/image_small'; }else { - avt = mail_utils.get_image( - session, 'res.users', 'image_small', session.uid); + avt = '/web/image/res.partner/' + session.uid + '/image_small'; } return avt; }, @@ -1388,7 +1379,7 @@ var ComposeMessage = Attachment.extend ({ 'id': result.id, 'name': result.name, 'filename': result.filename, - 'url': mail_utils.get_attachment_url(session, this.id, result.id) + 'url': '/web/content/' + result.id + '?download=true' }; } } diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index f7f4dd9b59b76..0f2353c729edf 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -241,7 +241,7 @@ var Followers = form_common.AbstractField.extend({ // truncate number of displayed followers _(this.followers).each(function (record) { $(qweb.render('mail_followers_partner', { - 'record': _.extend(record, {'avatar_url': mail_utils.get_image(session, record['res_model'], 'image_small', record['res_id'])}), + 'record': _.extend(record, {'avatar_url': '/web/image/' + record['res_model'] + '/' + record['res_id'] + '/image_small'}), 'widget': self}) ).appendTo(node_user_list); // On mouse-enter it will show the edit_subtype pencil. @@ -417,4 +417,4 @@ var Followers = form_common.AbstractField.extend({ /* Add the widget to registry */ core.form_widget_registry.add('mail_followers', Followers); -}); \ No newline at end of file +}); diff --git a/addons/mail/static/src/js/mail_utils.js b/addons/mail/static/src/js/mail_utils.js index b30968304aa70..656dc941a7531 100644 --- a/addons/mail/static/src/js/mail_utils.js +++ b/addons/mail/static/src/js/mail_utils.js @@ -27,23 +27,6 @@ function parse_email(text) { return [text, false]; } -/* Get an image in /web/binary/image?... */ -function get_image(session, model, field, id, resize) { - var r = resize ? encodeURIComponent(resize) : ''; - id = id || ''; - return session.url('/web/binary/image', {model: model, field: field, id: id, resize: r}); -} - -/* Get the url of an attachment {'id': id} */ -function get_attachment_url(session, message_id, attachment_id) { - return session.url('/mail/download_attachment', { - 'model': 'mail.message', - 'id': message_id, - 'method': 'download_attachment', - 'attachment_id': attachment_id - }); -} - /** * Replaces some expressions * - :name - shortcut to an image @@ -125,8 +108,6 @@ function bindTooltipTo($el, value, position) { return { parse_email: parse_email, - get_image: get_image, - get_attachment_url: get_attachment_url, do_replace_expressions: do_replace_expressions, get_text2html: get_text2html, expand_domain: expand_domain, diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 30257ccd390ac..d3197d7d11b8c 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -208,7 +208,7 @@
- +
diff --git a/addons/mail/tests/test_mail_message.py b/addons/mail/tests/test_mail_message.py index 33fad50bc23af..edd0f0eae9a03 100644 --- a/addons/mail/tests/test_mail_message.py +++ b/addons/mail/tests/test_mail_message.py @@ -186,11 +186,12 @@ def test_mail_message_access_read_notification(self): 'datas': 'My attachment'.encode('base64'), 'name': 'doc.txt', 'datas_fname': 'doc.txt'}) + # attach the attachment to the message self.message.write({'attachment_ids': [(4, attachment.id)]}) self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]}) self.message.sudo(self.user_employee).read() - # Test: Bert downloads attachment, ok because he can read message - self.message.sudo(self.user_employee).download_attachment(attachment.id) + # Test: Bert has access to attachment, ok because he can read message + attachment.sudo(self.user_employee).read(['name', 'datas']) def test_mail_message_access_read_author(self): self.message.write({'author_id': self.user_employee.partner_id.id}) diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 0b9a2168d2850..17eb7c295466f 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -624,7 +624,7 @@ var ProductCategoriesWidget = PosBaseWidget.extend({ }, get_image_url: function(category){ - return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id; + return window.location.origin + '/web/image?model=pos.category&field=image_medium&id='+category.id; }, render_category: function( category, with_image ){ @@ -771,7 +771,7 @@ var ProductListWidget = PosBaseWidget.extend({ this.renderElement(); }, get_product_image_url: function(product){ - return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id; + return window.location.origin + '/web/image?model=product.product&field=image_medium&id='+product.id; }, replace: function($target){ this.renderElement(); @@ -1109,7 +1109,7 @@ var ClientListScreenWidget = ScreenWidget.extend({ } }, partner_icon_url: function(id){ - return '/web/binary/image?model=res.partner&id='+id+'&field=image_small'; + return '/web/image?model=res.partner&id='+id+'&field=image_small'; }, // ui handle for the 'edit selected customer' action diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js index 732db3cc9d871..2e63bedadca0b 100644 --- a/addons/pos_restaurant/static/src/js/floors.js +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -375,7 +375,7 @@ var FloorScreenWidget = screens.ScreenWidget.extend({ } }, background_image_url: function(floor) { - return '/web/binary/image?model=restaurant.floor&id='+floor.id+'&field=background_image'; + return '/web/image?model=restaurant.floor&id='+floor.id+'&field=background_image'; }, get_floor_style: function() { var style = ""; diff --git a/addons/report/static/src/js/report.editor.js b/addons/report/static/src/js/report.editor.js index ea1e57d63ada6..a59c682316b5e 100644 --- a/addons/report/static/src/js/report.editor.js +++ b/addons/report/static/src/js/report.editor.js @@ -17,7 +17,7 @@ options.registry.many2one.include({ var $img = $('.header .row img:first'); var css = window.getComputedStyle($img[0]); $img.css("max-height", css.height+'px'); - $img.attr("src", "/web_editor/image/res.partner/"+self.ID+"/image"); + $img.attr("src", "/web/image/res.partner/"+self.ID+"/image"); setTimeout(function () { $img.removeClass('o_dirty'); },0); } } diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 61bf1199a150a..41698b725efb8 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -import ast import base64 import csv import functools @@ -39,7 +38,8 @@ from openerp.tools.translate import _ from openerp.tools import ustr from openerp import http -from openerp.http import request, serialize_exception as _serialize_exception +import mimetypes +from openerp.http import request, serialize_exception as _serialize_exception, STATIC_CACHE from openerp.exceptions import AccessError _logger = logging.getLogger(__name__) @@ -432,6 +432,82 @@ def content_disposition(filename): else: return "attachment; filename*=UTF-8''%s" % escaped +def binary_content(xmlid=None, model='ir.attachment', id=None, field='datas', unique=False, filename=None, filename_field='datas_fname', download=False, mimetype=None, default_mimetype='application/octet-stream', env=None): + """ Get file, attachment or downloadable content + + If the ``xmlid`` and ``id`` parameter is omitted, fetches the default value for the + binary field (via ``default_get``), otherwise fetches the field for + that precise record. + + :param str xmlid: xmlid of the record + :param str model: name of the model to fetch the binary from + :param int id: id of the record from which to fetch the binary + :param str field: binary field + :param bool unique: add a max-age for the cache control + :param str filename: choose a filename + :param str filename_field: if not create an filename with model-id-field + :param bool download: apply headers to download the file + :param str mimetype: mintype of the field (for headers) + :param str default_mimetype: default mintype if no mintype found + :param Environment env: by default use request.env + :returns: (status, headers, content) + """ + env = env or request.env + # get object and content + obj = None + if xmlid: + obj = env.ref(xmlid, False) + elif id and model in env.registry: + obj = env[model].browse(int(id)) + + # obj exists + if not obj or not obj.exists() or field not in obj: + return (404, [], None) + + # check read access + try: + last_update = obj['__last_update'] + except AccesError, e: + return (403, [], None) + status = 200 + + # filename + if not filename: + if filename_field in obj: + filename = obj[filename_field] + else: + filename = "%s-%s-%s" % (obj._model._name, obj.id, field) + + # mimetype + if not mimetype: + if 'mimetype' in obj and obj.mimetype: + mimetype = obj.mimetype + elif filename: + mimetype = mimetypes.guess_type(filename)[0] + if not mimetype: + mimetype = default_mimetype + headers = [('Content-Type', mimetype)] + + # cache + etag = hasattr(request, 'httprequest') and request.httprequest.headers.get('If-None-Match') + retag = hashlib.md5(last_update).hexdigest() + if etag == retag: + status = 304 + headers.append(('ETag', retag)) + + if unique: + headers.append(('Cache-Control', 'max-age=%s' % STATIC_CACHE)) + else: + headers.append(('Cache-Control', 'max-age=0')) + + # content-disposition default name + if download: + headers.append(('Content-Disposition', content_disposition(filename))) + + # get content after cache control + content = obj[field] or '' + + return (status, headers, content) #---------------------------------------------------------- # OpenERP Web web Controllers @@ -978,138 +1054,74 @@ def action(self, model, id): class Binary(http.Controller): - @http.route('/web/binary/image', type='http', auth="public") - def image(self, model, id, field, **kw): - last_update = '__last_update' - Model = request.registry[model] - cr, uid, context = request.cr, request.uid, request.context - headers = [('Content-Type', 'image/png')] - etag = request.httprequest.headers.get('If-None-Match') - hashed_session = hashlib.md5(request.session_id).hexdigest() - retag = hashed_session - id = None if not id else simplejson.loads(id) - if type(id) is list: - id = id[0] # m2o - try: - if etag: - if not id and hashed_session == etag: - return werkzeug.wrappers.Response(status=304) - else: - date = Model.read(cr, uid, [id], [last_update], context)[0].get(last_update) - if hashlib.md5(date).hexdigest() == etag: - return werkzeug.wrappers.Response(status=304) - - if not id: - res = Model.default_get(cr, uid, [field], context).get(field) - image_base64 = res - else: - res = Model.read(cr, uid, [id], [last_update, field], context)[0] - retag = hashlib.md5(res.get(last_update)).hexdigest() - image_base64 = res.get(field) - - if kw.get('resize'): - resize = kw.get('resize').split(',') - if len(resize) == 2 and int(resize[0]) and int(resize[1]): - width = int(resize[0]) - height = int(resize[1]) - # resize maximum 500*500 - if width > 500: width = 500 - if height > 500: height = 500 - image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG') - - image_data = base64.b64decode(image_base64) - - except Exception: - image_data = self.placeholder() - headers.append(('ETag', retag)) - headers.append(('Content-Length', len(image_data))) - try: - ncache = int(kw.get('cache')) - headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache))) - except: - pass - return request.make_response(image_data, headers) - def placeholder(self, image='placeholder.png'): addons_path = http.addons_manifest['web']['addons_path'] return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read() - @http.route('/web/binary/saveas', type='http', auth="public") - @serialize_exception - def saveas(self, model, field, id=None, filename_field=None, **kw): - """ Download link for files stored as binary fields. - - If the ``id`` parameter is omitted, fetches the default value for the - binary field (via ``default_get``), otherwise fetches the field for - that precise record. - - :param str model: name of the model to fetch the binary from - :param str field: binary field - :param str id: id of the record from which to fetch the binary - :param str filename_field: field holding the file's name, if any - :returns: :class:`werkzeug.wrappers.Response` - """ - Model = request.registry[model] - cr, uid, context = request.cr, request.uid, request.context - fields = [field] - content_type = 'application/octet-stream' - if filename_field: - fields.append(filename_field) - if id: - fields.append('file_type') - res = Model.read(cr, uid, [int(id)], fields, context)[0] - if res.get('file_type'): - content_type = res['file_type'] + @http.route(['/web/content', + '/web/content/', + '/web/content//', + '/web/content/', + '/web/content//', + '/web/content///', + '/web/content////'], type='http', auth="public") + def content_common(self, xmlid=None, model='ir.attachment', id=None, field='datas', filename=None, filename_field='datas_fname', unique=None, mimetype=None, download=None, data=None, token=None): + status, headers, content = binary_content(xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype) + if status == 304: + response = werkzeug.wrappers.Response(status=status, headers=headers) + elif status != 200: + response = request.not_found() else: - res = Model.default_get(cr, uid, fields, context) - filecontent = base64.b64decode(res.get(field) or '') - if not filecontent: + content_base64 = base64.b64decode(content) + headers.append(('Content-Length', len(content_base64))) + response = request.make_response(content_base64, headers) + if token: + response.set_cookie('fileToken', token) + return response + + @http.route(['/web/image', + '/web/image/', + '/web/image//', + '/web/image//x', + '/web/image//x/', + '/web/image/', + '/web/image//', + '/web/image///', + '/web/image////', + '/web/image//x', + '/web/image//x/', + '/web/image////x', + '/web/image////x/'], type='http', auth="public") + def content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas', filename_field='datas_fname', unique=None, filename=None, mimetype=None, download=None, width=0, height=0): + status, headers, content = binary_content(xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype, default_mimetype='image/png') + if status == 304: + return werkzeug.wrappers.Response(status=304, headers=headers) + elif status != 200 and download: return request.not_found() - else: - filename = '%s_%s' % (model.replace('.', '_'), id) - if filename_field: - filename = res.get(filename_field, '') or filename - return request.make_response(filecontent, - [('Content-Type', content_type), - ('Content-Disposition', content_disposition(filename))]) - - @http.route('/web/binary/saveas_ajax', type='http', auth="public") - @serialize_exception - def saveas_ajax(self, data, token): - jdata = simplejson.loads(data) - model = jdata['model'] - field = jdata['field'] - data = jdata['data'] - id = jdata.get('id', None) - filename_field = jdata.get('filename_field', None) - context = jdata.get('context', {}) - content_type = 'application/octet-stream' - Model = request.session.model(model) - fields = [field] - if filename_field: - fields.append(filename_field) - if data: - res = {field: data, filename_field: jdata.get('filename', None)} - elif id: - fields.append('file_type') - res = Model.read([int(id)], fields, context)[0] - if res.get('file_type'): - content_type = res['file_type'] - else: - res = Model.default_get(fields, context) - filecontent = base64.b64decode(res.get(field) or '') - if not filecontent: - raise ValueError(_("No content found for field '%s' on '%s:%s'") % - (field, model, id)) - else: - filename = '%s_%s' % (model.replace('.', '_'), id) - if filename_field: - filename = res.get(filename_field, '') or filename - return request.make_response(filecontent, - headers=[('Content-Type', content_type), - ('Content-Disposition', content_disposition(filename))], - cookies={'fileToken': token}) + if content and width and height: + # resize maximum 500*500 + if width > 500: + width = 500 + if height > 500: + height = 500 + content = openerp.tools.image_resize_image(base64_source=content, size=(width, height), encoding='base64', filetype='PNG') + + image_base64 = content and base64.b64decode(content) or self.placeholder() + headers.append(('Content-Length', len(image_base64))) + response = request.make_response(image_base64, headers) + response.status = str(status) + return response + + # backward compatibility + @http.route(['/web/binary/image'], type='http', auth="public") + def content_image_backward_compatibility(self, model, id, field, resize=None, **kw): + width = None + height = None + if resize: + width, height = resize.split(",") + return self.content_image(model=model, id=id, field=field, width=width, height=height) + @http.route('/web/binary/upload', type='http', auth="user") @serialize_exception @@ -1145,6 +1157,7 @@ def upload_attachment(self, callback, model, id, ufile): }, request.context) args = { 'filename': ufile.filename, + 'mimetype': ufile.content_type, 'id': attachment_id } except Exception: diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js index d1d1378b92be7..92d2360163c13 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -1462,7 +1462,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractManyField.extend(common.Reinitializ this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change ); }, get_file_url: function (attachment) { - return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment.id}); + return '/web/content/' + attachment.id + '?download=true'; }, read_name_values : function () { var self = this; @@ -1474,7 +1474,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractManyField.extend(common.Reinitializ return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) { _.each(datas, function (data) { data.no_unlink = true; - data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id}); + data.url = '/web/content/' + data.id + '?download=true'; self.data[data.id] = data; }); return ids; diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js index c386e6fc99373..9beb6a8c9a0de 100644 --- a/addons/web/static/src/js/views/form_view.js +++ b/addons/web/static/src/js/views/form_view.js @@ -333,6 +333,7 @@ var FormView = View.extend(common.FieldManagerMixin, { } var fields = _.keys(self.fields_view.fields); fields.push('display_name'); + fields.push('__last_update'); return self.dataset.read_index(fields, { context: { 'bin_size': true } }).then(function(r) { diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js index 7d00798148465..0d42f3d4469fe 100644 --- a/addons/web/static/src/js/views/form_widgets.js +++ b/addons/web/static/src/js/views/form_widgets.js @@ -1155,18 +1155,18 @@ var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, { var filename_fieldname = this.node.attrs.filename; var filename_field = this.view.fields && this.view.fields[filename_fieldname]; this.session.get_file({ - url: '/web/binary/saveas_ajax', - data: {data: JSON.stringify({ - model: this.view.dataset.model, - id: (this.view.datarecord.id || ''), - field: this.name, - filename_field: (filename_fieldname || ''), - data: utils.is_bin_size(value) ? null : value, - filename: filename_field ? filename_field.get('value') : null, - context: this.view.dataset.get_context() - })}, - complete: framework.unblockUI, - error: c.rpc_error.bind(c) + 'url': '/web/content', + 'data': { + 'model': this.view.dataset.model, + 'id': this.view.datarecord.id, + 'field': this.name, + 'filename_field': filename_fieldname, + 'filename': filename_field ? filename_field.get('value') : null, + 'download': true, + 'data': utils.is_bin_size(value) ? null : value, + }, + 'complete': framework.unblockUI, + 'error': c.rpc_error.bind(c) }); ev.stopPropagation(); return false; @@ -1263,11 +1263,11 @@ var FieldBinaryImage = FieldBinary.extend({ var field = this.name; if (this.options.preview_image) field = this.options.preview_image; - url = session.url('/web/binary/image', { + url = session.url('/web/image', { model: this.view.dataset.model, id: id, field: field, - t: (new Date().getTime()), + unique: (this.view.datarecord.__last_update || '').replace(/[^0-9]/g, ''), }); } else { url = this.placeholder; diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index ec21706bbcab2..63940726fca3c 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -1910,7 +1910,7 @@ var ColumnBinary = Column.extend({ if (value.substr(0, 10).indexOf(' ') == -1) { download_url = "data:application/octet-stream;base64," + value; } else { - download_url = session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id}); + download_url = session.url('/web/content', {model: options.model, field: this.id, id: options.id, download: true}); if (this.filename) { download_url += '&filename_field=' + this.filename; } diff --git a/addons/web/static/src/js/widgets/sidebar.js b/addons/web/static/src/js/widgets/sidebar.js index 9f7c46bf48dde..18f488cdfec20 100644 --- a/addons/web/static/src/js/widgets/sidebar.js +++ b/addons/web/static/src/js/widgets/sidebar.js @@ -174,16 +174,14 @@ var Sidebar = Widget.extend({ } }, on_attachments_loaded: function(attachments) { - var self = this; - var prefix = session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'}); _.each(attachments,function(a) { a.label = a.name; if(a.type === "binary") { - a.url = prefix + '&id=' + a.id + '&t=' + (new Date().getTime()); + a.url = '/web/content/' + a.id + '?download=true&t=' + (new Date().getTime()); } }); - self.items.files = attachments; - self.redraw(); + this.items.files = attachments; + this.redraw(); this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed); this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete); }, diff --git a/addons/web/static/src/js/widgets/user_menu.js b/addons/web/static/src/js/widgets/user_menu.js index 94a47cfa4b0e7..9111a9a34bae8 100644 --- a/addons/web/static/src/js/widgets/user_menu.js +++ b/addons/web/static/src/js/widgets/user_menu.js @@ -47,7 +47,7 @@ var SystrayMenu = Widget.extend({ if (!session.debug) { topbar_name = _.str.sprintf("%s (%s)", topbar_name, session.db); } - var avatar_src = session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: session.uid}); + var avatar_src = session.url('/web/image', {model:'res.users', field: 'image_small', id: session.uid}); $avatar.attr('src', avatar_src); core.bus.trigger('resize'); // Re-trigger the reflow logic diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js index 7fdb96f0c7c34..5232bf0842225 100644 --- a/addons/web_calendar/static/src/js/web_calendar.js +++ b/addons/web_calendar/static/src/js/web_calendar.js @@ -565,7 +565,7 @@ var CalendarView = View.extend({ if (attendee_showed<= MAX_ATTENDEES) { if (self.avatar_model !== null) { the_title_avatar += ''; + src="/web/image/' + self.avatar_model + '/' + the_attendee_people + '/image_small">'; } else { if (!self.colorIsAttendee || the_attendee_people != temp_ret[self.color_field]) { diff --git a/addons/web_calendar/static/src/xml/web_fullcalendar.xml b/addons/web_calendar/static/src/xml/web_fullcalendar.xml index 38dafdd577ed8..7aef9749e6307 100644 --- a/addons/web_calendar/static/src/xml/web_fullcalendar.xml +++ b/addons/web_calendar/static/src/xml/web_fullcalendar.xml @@ -28,7 +28,7 @@ - + diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 7d9b6206ae784..6f93d50c9d783 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -127,66 +127,6 @@ def export_icon_to_png(self, icon, color='#000', size=100, alpha=255, font='/web return response - #------------------------------------------------------ - # image route for browse record - #------------------------------------------------------ - def placeholder(self, response): - return request.registry['ir.attachment']._image_placeholder(response) - - @http.route([ - '/web_editor/image', - '/web_editor/image/', - '/web_editor/image//x', - '/web_editor/image//', - '/web_editor/image///x', - '/web_editor/image///', - '/web_editor/image////x' - ], type='http', auth="public") - def image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None): - """ Fetches the requested field and ensures it does not go above - (max_width, max_height), resizing it if necessary. - - If the record is not found or does not have the requested field, - returns a placeholder image via :meth:`~.placeholder`. - - Sets and checks conditional response parameters: - * :mailheader:`ETag` is always set (and checked) - * :mailheader:`Last-Modified is set iif the record has a concurrency - field (``__last_update``) - - The requested field is assumed to be base64-encoded image data in - all cases. - - xmlid can be used to load the image. But the field image must by base64-encoded - """ - if xmlid and "." in xmlid: - try: - record = request.env.ref(xmlid) - model, id = record._name, record.id - except: - raise werkzeug.exceptions.NotFound() - if model == 'ir.attachment' and not field: - if record.sudo().type == "url": - field = "url" - else: - field = "datas" - - if not model or not id or not field: - raise werkzeug.exceptions.NotFound() - - try: - idsha = str(id).split('_') - id = idsha[0] - response = werkzeug.wrappers.Response() - return request.registry['ir.attachment']._image( - request.cr, request.uid, model, id, field, response, max_width, max_height, - cache=STATIC_CACHE if len(idsha) > 1 else None) - except Exception: - logger.exception("Cannot render image field %r of record %s[%s] at size(%s,%s)", - field, model, id, max_width, max_height) - response = werkzeug.wrappers.Response() - return self.placeholder(response) - #------------------------------------------------------ # add attachment (images or link) #------------------------------------------------------ @@ -200,7 +140,6 @@ def attach(self, func, upload=None, url=None, disable_optimization=None, **kwarg uploads = [] message = None if not upload: # no image provided, storing the link and the image name - uploads.append({'website_url': url}) name = url.split("/").pop() # recover filename attachment_id = Attachments.create(request.cr, request.uid, { 'name': name, @@ -208,6 +147,7 @@ def attach(self, func, upload=None, url=None, disable_optimization=None, **kwarg 'url': url, 'res_model': 'ir.ui.view', }, request.context) + uploads.append({'id': attachment_id}) else: # images provided try: attachment_ids = [] @@ -230,10 +170,7 @@ def attach(self, func, upload=None, url=None, disable_optimization=None, **kwarg 'res_model': 'ir.ui.view', }, request.context) attachment_ids.append(attachment_id) - - uploads = Attachments.read( - request.cr, request.uid, attachment_ids, ['website_url'], - context=request.context) + uploads = [{'id': id} for id in attachment_ids] except Exception, e: logger.exception("Failed to upload image to attachment") message = unicode(e) diff --git a/addons/web_editor/models/ir_attachment.py b/addons/web_editor/models/ir_attachment.py index 3068807b2abbc..f489aa90aa2b2 100644 --- a/addons/web_editor/models/ir_attachment.py +++ b/addons/web_editor/models/ir_attachment.py @@ -28,122 +28,9 @@ def _local_url_get(self, cr, uid, ids, name, arg, context=None): if attach.url: result[attach.id] = attach.url else: - result[attach.id] = self.image_url(cr, uid, attach, 'datas') + result[attach.id] = '/web/image/%s?unique=%s' % (attach.id, attach.checksum) return result _columns = { 'local_url': fields.function(_local_url_get, string="Attachment URL", type='char'), } - - def _image_placeholder(self, response): - # file_open may return a StringIO. StringIO can be closed but are - # not context managers in Python 2 though that is fixed in 3 - with contextlib.closing(openerp.tools.misc.file_open( - os.path.join('web', 'static', 'src', 'img', 'placeholder.png'), - mode='rb')) as f: - response.data = f.read() - return response.make_conditional(request.httprequest) - - def _image(self, cr, uid, model, id_or_ids, field, response, max_width=maxint, max_height=maxint, cache=None, context=None): - """ Fetches the requested field and ensures it does not go above - (max_width, max_height), resizing it if necessary. - - Resizing is bypassed if the object provides a $field_big, which will - be interpreted as a pre-resized version of the base field. - - If the record is not found or does not have the requested field, - returns a placeholder image via :meth:`~._image_placeholder`. - - Sets and checks conditional response parameters: - * :mailheader:`ETag` is always set (and checked) - * :mailheader:`Last-Modified is set iif the record has a concurrency - field (``__last_update``) - - The requested field is assumed to be base64-encoded image data in - all cases. - """ - Model = self.pool[model] - ids = isinstance(id_or_ids, (list, tuple)) and id_or_ids or [int(id_or_ids)] - ids = Model.search(cr, uid, [('id', 'in', ids)], context=context) - - if not ids: - return self._image_placeholder(response) - - concurrency = '__last_update' - [record] = Model.read(cr, openerp.SUPERUSER_ID, ids, [concurrency, field], context=context) - - if concurrency in record: - server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT - try: - response.last_modified = datetime.datetime.strptime( - record[concurrency], server_format + '.%f') - except ValueError: - # just in case we have a timestamp without microseconds - response.last_modified = datetime.datetime.strptime( - record[concurrency], server_format) - - # Field does not exist on model or field set to False - if not record.get(field): - # FIXME: maybe a field which does not exist should be a 404? - return self._image_placeholder(response) - - response.set_etag(hashlib.sha1(record[field]).hexdigest()) - response.make_conditional(request.httprequest) - - if cache: - response.cache_control.max_age = cache - response.expires = int(time.time() + cache) - - # conditional request match - if response.status_code == 304: - return response - - if model == 'ir.attachment' and field == 'url' and field in record: - path = record[field].strip('/') - - # Check that we serve a file from within the module - if os.path.normpath(path).startswith('..'): - return self._image_placeholder(response) - - # Check that the file actually exists - path = path.split('/') - resource = openerp.modules.get_module_resource(*path) - if not resource: - return self._image_placeholder(response) - - data = open(resource, 'rb').read() - else: - data = record[field].decode('base64') - image = Image.open(cStringIO.StringIO(data)) - response.mimetype = Image.MIME[image.format] - - filename = '%s_%s.%s' % (model.replace('.', '_'), ids[0], str(image.format).lower()) - response.headers['Content-Disposition'] = 'inline; filename="%s"' % filename - - if (not max_width) and (not max_height): - response.data = data - return response - - w, h = image.size - max_w = int(max_width) if max_width else maxint - max_h = int(max_height) if max_height else maxint - - if w < max_w and h < max_h: - response.data = data - else: - size = (max_w, max_h) - img = image_resize_and_sharpen(image, size, preserve_aspect_ratio=True) - image_save_for_web(img, response.stream, format=image.format) - # invalidate content-length computed by make_conditional as - # writing to response.stream does not do it (as of werkzeug 0.9.3) - del response.headers['Content-Length'] - - return response - - def image_url(self, cr, uid, record, field, size=None, context=None): - """Returns a local url that points to the image field of a given browse record.""" - model = record._name - sudo_record = record.sudo() - id = '%s_%s' % (record.id, hashlib.sha1(sudo_record.write_date or sudo_record.create_date or '').hexdigest()[0:7]) - size = '' if size is None else '/%s' % size - return '/web_editor/image/%s/%s/%s%s' % (model, id, field, size) diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py index e4afb5a6cefd1..87dd6e5822633 100644 --- a/addons/web_editor/models/ir_qweb.py +++ b/addons/web_editor/models/ir_qweb.py @@ -15,6 +15,7 @@ import urlparse import re import simplejson +import hashlib import pytz from dateutil import parser @@ -329,13 +330,16 @@ def record_to_html(self, cr, uid, field_name, record, options=None, context=None classes = ' '.join(itertools.imap(escape, aclasses)) max_size = None - max_width, max_height = options.get('max_width', 0), options.get('max_height', 0) - if max_width or max_height: - max_size = '%sx%s' % (max_width, max_height) - - src = self.pool['ir.attachment'].image_url(cr, uid, record, field_name, max_size) if options.get('resize'): - src = "%s/%s" % (src, options.get('resize')) + max_size = options.get('resize') + else: + max_width, max_height = options.get('max_width', 0), options.get('max_height', 0) + if max_width or max_height: + max_size = '%sx%s' % (max_width, max_height) + + sha = hashlib.sha1(record.write_date).hexdigest()[0:7] + max_size = '' if max_size is None else '/%s' % max_size + src = '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field_name, max_size, sha) img = '' % (classes, src, options.get('style', '')) return ir_qweb.HTMLSafe(img) @@ -346,8 +350,8 @@ def from_html(self, cr, uid, model, field, element, context=None): url = element.find('img').get('src') url_object = urlparse.urlsplit(url) - if url_object.path.startswith('/web_editor/image'): - # url might be /web_editor/image//[_]/[/x] + if url_object.path.startswith('/web/image'): + # url might be /web/image//[_]/[/x] fragments = url_object.path.split('/') query = dict(urlparse.parse_qsl(url_object.query)) model = query.get('model', fragments[3]) diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js index 87d985f19f465..7f18bde35f06d 100644 --- a/addons/web_editor/static/src/js/widgets.js +++ b/addons/web_editor/static/src/js/widgets.js @@ -366,7 +366,7 @@ var ImageDialog = Widget.extend({ self.file_selected(null, error || !attachments.length); } for (var i=0; i
- +
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 9a8797fafd859..53a4edabceb96 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -369,17 +369,3 @@ def actions_server(self, path_or_xml_id_or_id, **post): if res: return res return request.redirect('/') - - #------------------------------------------------------ - # image route for browse record - #------------------------------------------------------ - @http.route([ - '/website/image', - '/website/image/', - '/website/image//', - '/website/image///', - '/website/image////x' - ], type='http', auth="public") - def website_image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None): - logger.warning("Deprecated image controller, please use /web_editor/image/") - return Web_Editor().image(model=model, id=id, field=field, xmlid=xmlid, max_width=max_width, max_height=max_height) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 33ccafecd9c52..3c57c03ac50be 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -5,6 +5,7 @@ import unicodedata import re import urlparse +import hashlib from sys import maxint @@ -122,8 +123,8 @@ def slug(value): DEFAULT_CDN_FILTERS = [ "^/[^/]+/static/", "^/web/(css|js)/", - "^/website/image/", - "^/web_editor/image/", + "^/web/image", + "^/web/content", ] def unslug(s): @@ -558,7 +559,11 @@ def _image(self, cr, uid, model, id, field, response, max_width=maxint, max_heig return self.pool['ir.attachment']._image(cr, uid, model, id, field, response, max_width=max_width, max_height=max_height, cache=cache, context=context) def image_url(self, cr, uid, record, field, size=None, context=None): - return self.pool['ir.attachment'].image_url(cr, uid, record, field, size=size, context=context) + """Returns a local url that points to the image field of a given browse record.""" + sudo_record = record.sudo() + sha = hashlib.sha1(sudo_record.write_date).hexdigest()[0:7] + size = '' if size is None else '/%s' % size + return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha) class website_menu(osv.osv): _name = "website.menu" @@ -637,17 +642,6 @@ class ir_attachment(osv.osv): 'website_url': fields.related("local_url", string="Attachment URL", type='char', deprecated=True), # related for backward compatibility with saas-6 } - def _image(self, cr, uid, model, id_or_ids, field, response, max_width=maxint, max_height=maxint, cache=None, context=None): - Model = self.pool[model] - ids = isinstance(id_or_ids, (list, tuple)) and id_or_ids or [int(id_or_ids)] - ids = Model.search(cr, uid, [('id', 'in', ids)], context=context) - - if not ids and 'website_published' in Model._fields: - ids = Model.search(cr, openerp.SUPERUSER_ID, [('id', 'in', ids), ('website_published', '=', True)], context=context) - - return super(ir_attachment, self)._image(cr, openerp.SUPERUSER_ID, model, id_or_ids, field, response, - max_width=max_width, max_height=max_height, cache=cache, context=context) - class res_partner(osv.osv): _inherit = "res.partner" diff --git a/addons/website_blog/static/src/js/website_blog.editor.js b/addons/website_blog/static/src/js/website_blog.editor.js index 5effc1e6c642d..2bfbb42754c20 100644 --- a/addons/website_blog/static/src/js/website_blog.editor.js +++ b/addons/website_blog/static/src/js/website_blog.editor.js @@ -73,7 +73,7 @@ odoo.define('website_blog.editor', function (require) { var $img = $(this).find("img"); var css = window.getComputedStyle($img[0]); $img.css({ width: css.width, height: css.height }); - $img.attr("src", "/web_editor/image/res.partner/"+self.ID+"/image"); + $img.attr("src", "/web/image/res.partner/"+self.ID+"/image"); }); setTimeout(function () { $nodes.removeClass('o_dirty'); },0); } diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index 9fa59a79eee99..7cb116276397e 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -6,12 +6,14 @@ import simplejson import lxml from urllib2 import urlopen, URLError +import base64 +import openerp from openerp import tools, _ from openerp.addons.web import http +from openerp.addons.web.controllers.main import binary_content from openerp.addons.web.http import request from openerp.addons.website.models.website import slug -from openerp.tools.translate import _ class WebsiteForum(http.Controller): @@ -455,13 +457,14 @@ def open_partner(self, forum, partner_id=0, **post): @http.route(['/forum/user//avatar'], type='http', auth="public", website=True) def user_avatar(self, user_id=0, **post): - response = werkzeug.wrappers.Response() - User = request.env['res.users'] - Website = request.env['website'] - user = User.sudo().search([('id', '=', user_id)]) - if not user.exists() or (user_id != request.session.uid and user.karma < 1): - return Website._image_placeholder(response) - return Website._image('res.users', user.id, 'image', response) + status, headers, content = binary_content(model='res.users', id=user_id, field='image', default_mimetype='image/png', env=request.env(openerp.SUPERUSER_ID)) + if status == 304: + return werkzeug.wrappers.Response(status=304) + image_base64 = base64.b64decode(content) + headers.append(('Content-Length', len(image_base64))) + response = request.make_response(image_base64, headers) + response.status = str(status) + return response @http.route(['/forum//user/'], type='http', auth="public", website=True) def open_user(self, forum, user_id=0, **post): diff --git a/addons/website_mail/views/website_mail.xml b/addons/website_mail/views/website_mail.xml index 5dabf4149688c..8014a3db5e272 100644 --- a/addons/website_mail/views/website_mail.xml +++ b/addons/website_mail/views/website_mail.xml @@ -42,7 +42,7 @@
- +
diff --git a/addons/website_mail_channel/views/website_mail_channel.xml b/addons/website_mail_channel/views/website_mail_channel.xml index d1b9ba9ca01f8..60928ee381994 100644 --- a/addons/website_mail_channel/views/website_mail_channel.xml +++ b/addons/website_mail_channel/views/website_mail_channel.xml @@ -205,9 +205,9 @@