From 5cf29dcff2280b3439b82d6844d57c5c73bed34a Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Fri, 11 Dec 2015 13:01:49 +0100 Subject: [PATCH] [FIX] web, website: overrideable images controller The `binay_image` and `content_disposition` are moved from the web controller to the `ir.http` model, to be able to override these methods. This makes possible to browse the records images as sudo when the record is published on the website. e.g. to see the partner images on the website `/partners` page from the `website_crm_partner_assign` module. opw-659244 --- addons/web/controllers/main.py | 116 ++--------------------------- addons/website/models/ir_http.py | 12 +++ openerp/addons/base/ir/ir_http.py | 117 +++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 110 deletions(-) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 290e89e14e3..1c3723fa3b4 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -15,7 +15,6 @@ import re import json import sys import time -import urllib2 import zlib from xml.etree import ElementTree from cStringIO import StringIO @@ -33,13 +32,12 @@ except ImportError: import openerp import openerp.modules.registry from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebTemplateNotFound -from openerp.modules import get_module_path, get_resource_path +from openerp.modules import get_resource_path from openerp.tools import topological_sort from openerp.tools.translate import _ from openerp.tools import ustr from openerp import http -import mimetypes -from openerp.http import request, serialize_exception as _serialize_exception, STATIC_CACHE +from openerp.http import request, serialize_exception as _serialize_exception from openerp.exceptions import AccessError _logger = logging.getLogger(__name__) @@ -421,113 +419,13 @@ def xml2json_from_elementtree(el, preserve_whitespaces=False): return res def content_disposition(filename): - filename = ustr(filename) - escaped = urllib2.quote(filename.encode('utf8')) - browser = request.httprequest.user_agent.browser - version = int((request.httprequest.user_agent.version or '0').split('.')[0]) - if browser == 'msie' and version < 9: - return "attachment; filename=%s" % escaped - elif browser == 'safari' and version < 537: - return u"attachment; filename=%s" % filename.encode('ascii', 'replace') - else: - return "attachment; filename*=UTF-8''%s" % escaped + return request.registry['ir.http'].content_disposition(filename) + 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 AccessError: - return (403, [], None) - - status, headers, content = None, [], None - - # attachment by url check - module_resource_path = None - if model == 'ir.attachment' and obj.type == 'url' and obj.url: - url_match = re.match("^/(\w+)/(.+)$", obj.url) - if url_match: - module = url_match.group(1) - module_path = get_module_path(module) - module_resource_path = get_resource_path(module, url_match.group(2)) - if module_path and module_resource_path: - module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/' - module_resource_path = os.path.normpath(module_resource_path) - if module_resource_path.startswith(module_path): - with open(module_resource_path, 'r') as f: - content = base64.b64encode(f.read()) - last_update = str(os.path.getmtime(module_resource_path)) - - if not module_resource_path: - module_resource_path = obj.url - - if not content: - status = 301 - content = module_resource_path - else: - content = obj[field] or '' - - # filename - if not filename: - if filename_field in obj: - filename = obj[filename_field] - elif module_resource_path: - filename = os.path.basename(module_resource_path) - else: - filename = "%s-%s-%s" % (obj._model._name, obj.id, field) - - # mimetype - if not mimetype: - if 'mimetype' in obj and obj.mimetype and obj.mimetype != 'application/octet-stream': - mimetype = obj.mimetype - elif filename: - mimetype = mimetypes.guess_type(filename)[0] - if not mimetype: - mimetype = default_mimetype - headers.append(('Content-Type', mimetype)) - - # cache - etag = hasattr(request, 'httprequest') and request.httprequest.headers.get('If-None-Match') - retag = hashlib.md5(last_update).hexdigest() - status = status or (304 if etag == retag else 200) - headers.append(('ETag', retag)) - headers.append(('Cache-Control', 'max-age=%s' % (STATIC_CACHE if unique else 0))) - - # content-disposition default name - if download: - headers.append(('Content-Disposition', content_disposition(filename))) - - return (status, headers, content) + return request.registry['ir.http'].binary_content( + xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, + download=download, mimetype=mimetype, default_mimetype=default_mimetype, env=env) def db_info(): version_info = openerp.service.common.exp_version() diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 72d710d2fda..5b5f175338f 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -296,6 +296,18 @@ class ir_http(orm.AbstractModel): html = request.website._render('website.http_error', values) return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8') + def binary_content(self, 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): + env = env or request.env + obj = None + if xmlid: + obj = env.ref(xmlid, False) + elif id and model in self.pool: + obj = env[model].browse(int(id)) + if obj and 'website_published' in obj._fields: + if env[obj._name].sudo().search([('id', '=', obj.id), ('website_published', '=', True)]): + env = env(user=openerp.SUPERUSER_ID) + return super(ir_http, self).binary_content(xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype, default_mimetype=default_mimetype, env=env) + class ModelConverter(ir.ir_http.ModelConverter): def __init__(self, url_map, model=False, domain='[]'): super(ModelConverter, self).__init__(url_map, model) diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index f0bfd353f33..72e66e24c33 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -2,11 +2,15 @@ #---------------------------------------------------------- # ir_http modular http routing #---------------------------------------------------------- +import base64 import datetime import hashlib import logging +import mimetypes +import os import re import sys +import urllib2 import werkzeug import werkzeug.exceptions @@ -18,7 +22,8 @@ import openerp import openerp.exceptions import openerp.models from openerp import http -from openerp.http import request +from openerp.http import request, STATIC_CACHE +from openerp.modules.module import get_resource_path, get_module_path from openerp.osv import osv, orm _logger = logging.getLogger(__name__) @@ -203,6 +208,116 @@ class ir_http(osv.AbstractModel): return self._routing_map + def content_disposition(self, filename): + filename = openerp.tools.ustr(filename) + escaped = urllib2.quote(filename.encode('utf8')) + browser = request.httprequest.user_agent.browser + version = int((request.httprequest.user_agent.version or '0').split('.')[0]) + if browser == 'msie' and version < 9: + return "attachment; filename=%s" % escaped + elif browser == 'safari' and version < 537: + return u"attachment; filename=%s" % filename.encode('ascii', 'replace') + else: + return "attachment; filename*=UTF-8''%s" % escaped + + def binary_content(self, 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 openerp.exceptions.AccessError: + return (403, [], None) + + status, headers, content = None, [], None + + # attachment by url check + module_resource_path = None + if model == 'ir.attachment' and obj.type == 'url' and obj.url: + url_match = re.match("^/(\w+)/(.+)$", obj.url) + if url_match: + module = url_match.group(1) + module_path = get_module_path(module) + module_resource_path = get_resource_path(module, url_match.group(2)) + if module_path and module_resource_path: + module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/' + module_resource_path = os.path.normpath(module_resource_path) + if module_resource_path.startswith(module_path): + with open(module_resource_path, 'r') as f: + content = base64.b64encode(f.read()) + last_update = str(os.path.getmtime(module_resource_path)) + + if not module_resource_path: + module_resource_path = obj.url + + if not content: + status = 301 + content = module_resource_path + else: + content = obj[field] or '' + + # filename + if not filename: + if filename_field in obj: + filename = obj[filename_field] + elif module_resource_path: + filename = os.path.basename(module_resource_path) + else: + filename = "%s-%s-%s" % (obj._model._name, obj.id, field) + + # mimetype + if not mimetype: + if 'mimetype' in obj and obj.mimetype and obj.mimetype != 'application/octet-stream': + mimetype = obj.mimetype + elif filename: + mimetype = mimetypes.guess_type(filename)[0] + if not mimetype: + mimetype = default_mimetype + headers.append(('Content-Type', mimetype)) + + # cache + etag = hasattr(request, 'httprequest') and request.httprequest.headers.get('If-None-Match') + retag = hashlib.md5(last_update).hexdigest() + status = status or (304 if etag == retag else 200) + headers.append(('ETag', retag)) + headers.append(('Cache-Control', 'max-age=%s' % (STATIC_CACHE if unique else 0))) + + # content-disposition default name + if download: + headers.append(('Content-Disposition', self.content_disposition(filename))) + + return (status, headers, content) + + def convert_exception_to(to_type, with_message=False): """ Should only be called from an exception handler. Fetches the current exception data from sys.exc_info() and creates a new exception of type