[FIX] check after openerp's AccessErrors in website's _handle_exception
[odoo/odoo.git] / addons / website / models / ir_http.py
1 # -*- coding: utf-8 -*-
2 import logging
3 import re
4 import traceback
5
6 import werkzeug
7 import werkzeug.routing
8
9 import openerp
10 from openerp.addons.base import ir
11 from openerp.addons.base.ir import ir_qweb
12 from openerp.addons.website.models.website import slug
13 from openerp.http import request
14 from openerp.osv import orm
15
16 logger = logging.getLogger(__name__)
17
18 class RequestUID(object):
19     def __init__(self, **kw):
20         self.__dict__.update(kw)
21
22 class ir_http(orm.AbstractModel):
23     _inherit = 'ir.http'
24
25     rerouting_limit = 10
26
27     def _get_converters(self):
28         return dict(
29             super(ir_http, self)._get_converters(),
30             model=ModelConverter,
31             page=PageConverter,
32         )
33
34     def _dispatch(self):
35         first_pass = not hasattr(request, 'website')
36         request.website = None
37         func = None
38         try:
39             func, arguments = self._find_handler()
40             request.website_enabled = func.routing.get('website', False)
41         except werkzeug.exceptions.NotFound:
42             # either we have a language prefixed route, either a real 404
43             # in all cases, website processes them
44             request.website_enabled = True
45
46         if request.website_enabled:
47             if func:
48                 self._authenticate(func.routing['auth'])
49             else:
50                 self._auth_method_public()
51             request.website = request.registry['website'].get_current_website(request.cr, request.uid, context=request.context)
52             if first_pass:
53                 request.lang = request.website.default_lang_code
54             request.context['lang'] = request.lang
55             request.website.preprocess_request(request)
56             if not func:
57                 path = request.httprequest.path.split('/')
58                 langs = [lg[0] for lg in request.website.get_languages()]
59                 if path[1] in langs:
60                     request.lang = request.context['lang'] = path.pop(1)
61                     path = '/'.join(path) or '/'
62                     return self.reroute(path)
63         return super(ir_http, self)._dispatch()
64
65     def reroute(self, path):
66         if not hasattr(request, 'rerouting'):
67             request.rerouting = [request.httprequest.path]
68         if path in request.rerouting:
69             raise Exception("Rerouting loop is forbidden")
70         request.rerouting.append(path)
71         if len(request.rerouting) > self.rerouting_limit:
72             raise Exception("Rerouting limit exceeded")
73         request.httprequest.environ['PATH_INFO'] = path
74         # void werkzeug cached_property. TODO: find a proper way to do this
75         for key in ('path', 'full_path', 'url', 'base_url'):
76             request.httprequest.__dict__.pop(key, None)
77
78         return self._dispatch()
79
80     def _postprocess_args(self, arguments):
81         if hasattr(request, 'rerouting'):
82             url = request.rerouting[0]
83         else:
84             url = request.httprequest.url
85         original_url = url
86         for arg in arguments.itervalues():
87             if isinstance(arg, orm.browse_record) and isinstance(arg._uid, RequestUID):
88                 placeholder = arg._uid
89                 arg._uid = request.uid
90                 try:
91                     good_slug = slug(arg)
92                     if str(arg.id) != placeholder.value and placeholder.value != good_slug:
93                         # TODO: properly recompose the url instead of using replace()
94                         url = url.replace(placeholder.value, good_slug)
95                 except KeyError, e:
96                     return self._handle_exception(e, code=404)
97         if url != original_url:
98             werkzeug.exceptions.abort(werkzeug.utils.redirect(url))
99
100     def _handle_exception(self, exception, code=500):
101         is_website_request = bool(getattr(request, 'website_enabled', False) and request.website)
102         if not is_website_request:
103             # Don't touch non website requests exception handling
104             return super(ir_http, self)._handle_exception(exception)
105         else:
106             try:
107                 response = super(ir_http, self)._handle_exception(exception)
108                 if isinstance(response, Exception):
109                     exception = response
110                 else:
111                     # if parent excplicitely returns a plain response, then we don't touch it
112                     return response
113             except Exception, e:
114                 exception = e
115
116             values = dict(
117                 exception=exception,
118                 traceback=traceback.format_exc(exception),
119             )
120             code = getattr(exception, 'code', code)
121
122             if isinstance(exception, openerp.exceptions.AccessError):
123                 code = 403
124
125             if isinstance(exception, ir_qweb.QWebException):
126                 values.update(qweb_exception=exception)
127                 if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
128                     code = 403
129
130             if code == 500:
131                 logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
132                 if 'qweb_exception' in values:
133                     view = request.registry.get("ir.ui.view")
134                     views = view._views_get(request.cr, request.uid, exception.qweb['template'], request.context)
135                     to_reset = [v for v in views if v.model_data_id.noupdate is True]
136                     values['views'] = to_reset
137             elif code == 403:
138                 logger.warn("403 Forbidden:\n\n%s", values['traceback'])
139
140             values.update(
141                 status_message=werkzeug.http.HTTP_STATUS_CODES[code],
142                 status_code=code,
143             )
144
145             if not request.uid:
146                 self._auth_method_public()
147
148             try:
149                 html = request.website._render('website.%s' % code, values)
150             except Exception:
151                 html = request.website._render('website.http_error', values)
152             return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
153
154 class ModelConverter(ir.ir_http.ModelConverter):
155     def __init__(self, url_map, model=False):
156         super(ModelConverter, self).__init__(url_map, model)
157         self.regex = r'(?:[A-Za-z0-9-_]+?-)?(\d+)(?=$|/)'
158
159     def to_url(self, value):
160         return slug(value)
161
162     def to_python(self, value):
163         m = re.match(self.regex, value)
164         _uid = RequestUID(value=value, match=m, converter=self)
165         return request.registry[self.model].browse(
166             request.cr, _uid, int(m.group(1)), context=request.context)
167
168     def generate(self, cr, uid, query=None, context=None):
169         return request.registry[self.model].name_search(
170             cr, uid, name=query or '', context=context)
171
172 class PageConverter(werkzeug.routing.PathConverter):
173     """ Only point of this converter is to bundle pages enumeration logic
174
175     Sads got: no way to get the view's human-readable name even if one exists
176     """
177     def generate(self, cr, uid, query=None, context=None):
178         View = request.registry['ir.ui.view']
179         views = View.search_read(
180             cr, uid, [['page', '=', True]],
181             fields=[], order='name', context=context)
182         xids = View.get_external_id(
183             cr, uid, [view['id'] for view in views], context=context)
184
185         for view in views:
186             xid = xids[view['id']]
187             if xid and (not query or query.lower() in xid.lower()):
188                 yield xid