[FIX] website: default mimetype for web-published attachments, avoids crash
[odoo/odoo.git] / addons / website / models / ir_http.py
1 # -*- coding: utf-8 -*-
2 import datetime
3 import hashlib
4 import logging
5 import re
6 import traceback
7 import werkzeug
8 import werkzeug.routing
9
10 import openerp
11 from openerp.addons.base import ir
12 from openerp.addons.base.ir import ir_qweb
13 from openerp.addons.website.models.website import slug
14 from openerp.http import request
15 from openerp.osv import orm
16
17 logger = logging.getLogger(__name__)
18
19 class RequestUID(object):
20     def __init__(self, **kw):
21         self.__dict__.update(kw)
22
23 class ir_http(orm.AbstractModel):
24     _inherit = 'ir.http'
25
26     rerouting_limit = 10
27
28     def _get_converters(self):
29         return dict(
30             super(ir_http, self)._get_converters(),
31             model=ModelConverter,
32             page=PageConverter,
33         )
34
35     def _dispatch(self):
36         first_pass = not hasattr(request, 'website')
37         request.website = None
38         func = None
39         try:
40             func, arguments = self._find_handler()
41             request.website_enabled = func.routing.get('website', False)
42         except werkzeug.exceptions.NotFound:
43             # either we have a language prefixed route, either a real 404
44             # in all cases, website processes them
45             request.website_enabled = True
46
47         if request.website_enabled:
48             if func:
49                 self._authenticate(func.routing['auth'])
50             else:
51                 self._auth_method_public()
52             request.website = request.registry['website'].get_current_website(request.cr, request.uid, context=request.context)
53             if first_pass:
54                 request.lang = request.website.default_lang_code
55             request.context['lang'] = request.lang
56             request.website.preprocess_request(request)
57             if not func:
58                 path = request.httprequest.path.split('/')
59                 langs = [lg[0] for lg in request.website.get_languages()]
60                 if path[1] in langs:
61                     request.lang = request.context['lang'] = path.pop(1)
62                     path = '/'.join(path) or '/'
63                     return self.reroute(path)
64         return super(ir_http, self)._dispatch()
65
66     def reroute(self, path):
67         if not hasattr(request, 'rerouting'):
68             request.rerouting = [request.httprequest.path]
69         if path in request.rerouting:
70             raise Exception("Rerouting loop is forbidden")
71         request.rerouting.append(path)
72         if len(request.rerouting) > self.rerouting_limit:
73             raise Exception("Rerouting limit exceeded")
74         request.httprequest.environ['PATH_INFO'] = path
75         # void werkzeug cached_property. TODO: find a proper way to do this
76         for key in ('path', 'full_path', 'url', 'base_url'):
77             request.httprequest.__dict__.pop(key, None)
78
79         return self._dispatch()
80
81     def _postprocess_args(self, arguments, rule):
82         if not getattr(request, 'website_enabled', False):
83             return super(ir_http, self)._postprocess_args(arguments, rule)
84
85         for arg, val in arguments.items():
86             # Replace uid placeholder by the current request.uid
87             if isinstance(val, orm.browse_record) and isinstance(val._uid, RequestUID):
88                 val._uid = request.uid
89         try:
90             _, path = rule.build(arguments)
91             assert path is not None
92         except Exception, e:
93             return self._handle_exception(e, code=404)
94
95         if request.httprequest.method in ('GET', 'HEAD'):
96             generated_path = werkzeug.url_unquote_plus(path)
97             current_path = werkzeug.url_unquote_plus(request.httprequest.path)
98             if generated_path != current_path:
99                 if request.lang != request.website.default_lang_code:
100                     path = '/' + request.lang + path
101                 return werkzeug.utils.redirect(path)
102
103     def _serve_attachment(self):
104         domain = [('type', '=', 'binary'), ('url', '=', request.httprequest.path)]
105         attach = self.pool['ir.attachment'].search_read(request.cr, openerp.SUPERUSER_ID, domain, ['__last_update', 'datas', 'mimetype'], context=request.context)
106         if attach:
107             wdate = attach[0]['__last_update']
108             datas = attach[0]['datas']
109             response = werkzeug.wrappers.Response()
110             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
111             try:
112                 response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f')
113             except ValueError:
114                 # just in case we have a timestamp without microseconds
115                 response.last_modified = datetime.datetime.strptime(wdate, server_format)
116
117             response.set_etag(hashlib.sha1(datas).hexdigest())
118             response.make_conditional(request.httprequest)
119
120             if response.status_code == 304:
121                 return response
122
123             response.mimetype = attach[0]['mimetype'] or 'application/octet-stream'
124             response.data = datas.decode('base64')
125             return response
126
127     def _handle_exception(self, exception, code=500):
128         # This is done first as the attachment path may
129         # not match any HTTP controller, so the request
130         # may not be website-enabled.
131         attach = self._serve_attachment()
132         if attach:
133             return attach
134
135         is_website_request = bool(getattr(request, 'website_enabled', False) and request.website)
136         if not is_website_request:
137             # Don't touch non website requests exception handling
138             return super(ir_http, self)._handle_exception(exception)
139         else:
140             try:
141                 response = super(ir_http, self)._handle_exception(exception)
142                 if isinstance(response, Exception):
143                     exception = response
144                 else:
145                     # if parent excplicitely returns a plain response, then we don't touch it
146                     return response
147             except Exception, e:
148                 exception = e
149
150             values = dict(
151                 exception=exception,
152                 traceback=traceback.format_exc(exception),
153             )
154             code = getattr(exception, 'code', code)
155
156             if isinstance(exception, openerp.exceptions.AccessError):
157                 code = 403
158
159             if isinstance(exception, ir_qweb.QWebException):
160                 values.update(qweb_exception=exception)
161                 if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
162                     code = 403
163
164             if code == 500:
165                 logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
166                 if 'qweb_exception' in values:
167                     view = request.registry.get("ir.ui.view")
168                     views = view._views_get(request.cr, request.uid, exception.qweb['template'], request.context)
169                     to_reset = [v for v in views if v.model_data_id.noupdate is True]
170                     values['views'] = to_reset
171             elif code == 403:
172                 logger.warn("403 Forbidden:\n\n%s", values['traceback'])
173
174             values.update(
175                 status_message=werkzeug.http.HTTP_STATUS_CODES[code],
176                 status_code=code,
177             )
178
179             if not request.uid:
180                 self._auth_method_public()
181
182             try:
183                 html = request.website._render('website.%s' % code, values)
184             except Exception:
185                 html = request.website._render('website.http_error', values)
186             return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
187
188 class ModelConverter(ir.ir_http.ModelConverter):
189     def __init__(self, url_map, model=False, domain='[]'):
190         super(ModelConverter, self).__init__(url_map, model)
191         self.domain = domain
192         self.regex = r'(?:[A-Za-z0-9-_]+?-)?(\d+)(?=$|/)'
193
194     def to_url(self, value):
195         return slug(value)
196
197     def to_python(self, value):
198         m = re.match(self.regex, value)
199         _uid = RequestUID(value=value, match=m, converter=self)
200         return request.registry[self.model].browse(
201             request.cr, _uid, int(m.group(1)), context=request.context)
202
203     def generate(self, cr, uid, query=None, args=None, context=None):
204         obj = request.registry[self.model]
205         domain = eval( self.domain, (args or {}).copy())
206         if query:
207             domain.append((obj._rec_name, 'ilike', '%'+query+'%'))
208         for record in obj.search_read(cr, uid, domain=domain, fields=['write_date',obj._rec_name], context=context):
209             if record.get(obj._rec_name, False):
210                 yield {'loc': (record['id'], record[obj._rec_name])}
211
212 class PageConverter(werkzeug.routing.PathConverter):
213     """ Only point of this converter is to bundle pages enumeration logic """
214     def generate(self, cr, uid, query=None, args={}, context=None):
215         View = request.registry['ir.ui.view']
216         views = View.search_read(cr, uid, [['page', '=', True]],
217             fields=['xml_id','priority','write_date'], order='name', context=context)
218         for view in views:
219             xid = view['xml_id'].startswith('website.') and view['xml_id'][8:] or view['xml_id']
220             # the 'page/homepage' url is indexed as '/', avoid aving the same page referenced twice
221             # when we will have an url mapping mechanism, replace this by a rule: page/homepage --> /
222             if xid=='homepage': continue
223             if query and query.lower() not in xid.lower():
224                 continue
225             record = {'loc': xid}
226             if view['priority'] <> 16:
227                 record['__priority'] = min(round(view['priority'] / 32.0,1), 1)
228             if view.get('write_date'):
229                 record['__lastmod'] = view['write_date'][:10]
230             yield record