2a774939c601c9544981557579ad5544ad26be8b
[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, url_for
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 _auth_method_public(self):
36         # TODO: select user_id from matching website
37         if not request.session.uid:
38             request.uid = self.pool['ir.model.data'].xmlid_to_res_id(request.cr, openerp.SUPERUSER_ID, 'base.public_user')
39         else:
40             request.uid = request.session.uid
41
42     def _dispatch(self):
43         first_pass = not hasattr(request, 'website')
44         request.website = None
45         func = None
46         try:
47             func, arguments = self._find_handler()
48             request.website_enabled = func.routing.get('website', False)
49         except werkzeug.exceptions.NotFound:
50             # either we have a language prefixed route, either a real 404
51             # in all cases, website processes them
52             request.website_enabled = True
53
54         if request.website_enabled:
55             if func:
56                 self._authenticate(func.routing['auth'])
57             else:
58                 self._auth_method_public()
59             request.redirect = lambda url: werkzeug.utils.redirect(url_for(url))
60             request.website = request.registry['website'].get_current_website(request.cr, request.uid, context=request.context)
61             if first_pass:
62                 request.lang = request.website.default_lang_code
63             request.context['lang'] = request.lang
64             if not func:
65                 path = request.httprequest.path.split('/')
66                 langs = [lg[0] for lg in request.website.get_languages()]
67                 if path[1] in langs:
68                     request.lang = request.context['lang'] = path.pop(1)
69                     path = '/'.join(path) or '/'
70                     return self.reroute(path)
71                 return self._handle_exception(code=404)
72         return super(ir_http, self)._dispatch()
73
74     def reroute(self, path):
75         if not hasattr(request, 'rerouting'):
76             request.rerouting = [request.httprequest.path]
77         if path in request.rerouting:
78             raise Exception("Rerouting loop is forbidden")
79         request.rerouting.append(path)
80         if len(request.rerouting) > self.rerouting_limit:
81             raise Exception("Rerouting limit exceeded")
82         request.httprequest.environ['PATH_INFO'] = path
83         # void werkzeug cached_property. TODO: find a proper way to do this
84         for key in ('path', 'full_path', 'url', 'base_url'):
85             request.httprequest.__dict__.pop(key, None)
86
87         return self._dispatch()
88
89     def _postprocess_args(self, arguments, rule):
90         if not getattr(request, 'website_enabled', False):
91             return super(ir_http, self)._postprocess_args(arguments, rule)
92
93         for arg, val in arguments.items():
94             # Replace uid placeholder by the current request.uid
95             if isinstance(val, orm.browse_record) and isinstance(val._uid, RequestUID):
96                 val._uid = request.uid
97         try:
98             _, path = rule.build(arguments)
99             assert path is not None
100         except Exception:
101             return self._handle_exception(werkzeug.exceptions.NotFound())
102
103         if request.httprequest.method in ('GET', 'HEAD'):
104             generated_path = werkzeug.url_unquote_plus(path)
105             current_path = werkzeug.url_unquote_plus(request.httprequest.path)
106             if generated_path != current_path:
107                 if request.lang != request.website.default_lang_code:
108                     path = '/' + request.lang + path
109                 return werkzeug.utils.redirect(path)
110
111     def _serve_attachment(self):
112         domain = [('type', '=', 'binary'), ('url', '=', request.httprequest.path)]
113         attach = self.pool['ir.attachment'].search_read(request.cr, openerp.SUPERUSER_ID, domain, ['__last_update', 'datas', 'mimetype'], context=request.context)
114         if attach:
115             wdate = attach[0]['__last_update']
116             datas = attach[0]['datas']
117             response = werkzeug.wrappers.Response()
118             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
119             try:
120                 response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f')
121             except ValueError:
122                 # just in case we have a timestamp without microseconds
123                 response.last_modified = datetime.datetime.strptime(wdate, server_format)
124
125             response.set_etag(hashlib.sha1(datas).hexdigest())
126             response.make_conditional(request.httprequest)
127
128             if response.status_code == 304:
129                 return response
130
131             response.mimetype = attach[0]['mimetype']
132             response.data = datas.decode('base64')
133             return response
134
135     def _handle_exception(self, exception=None, code=500):
136         if isinstance(exception, werkzeug.exceptions.HTTPException) and hasattr(exception, 'response') and exception.response:
137             return exception.response
138
139         attach = self._serve_attachment()
140         if attach:
141             return attach
142
143         if getattr(request, 'website_enabled', False) and request.website:
144             values = dict(
145                 exception=exception,
146                 traceback=traceback.format_exc(exception),
147             )
148             if exception:
149                 code = getattr(exception, 'code', code)
150                 if isinstance(exception, ir_qweb.QWebException):
151                     values.update(qweb_exception=exception)
152                     if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
153                         code = 403
154             if code == 500:
155                 logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
156                 if 'qweb_exception' in values:
157                     view = request.registry.get("ir.ui.view")
158                     views = view._views_get(request.cr, request.uid, exception.qweb['template'], request.context)
159                     to_reset = [v for v in views if v.model_data_id.noupdate is True]
160                     values['views'] = to_reset
161             elif code == 403:
162                 logger.warn("403 Forbidden:\n\n%s", values['traceback'])
163
164             values.update(
165                 status_message=werkzeug.http.HTTP_STATUS_CODES[code],
166                 status_code=code,
167             )
168
169             if not request.uid:
170                 self._auth_method_public()
171
172             try:
173                 html = request.website._render('website.%s' % code, values)
174             except Exception:
175                 html = request.website._render('website.http_error', values)
176             return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
177
178         return super(ir_http, self)._handle_exception(exception)
179
180 class ModelConverter(ir.ir_http.ModelConverter):
181     def __init__(self, url_map, model=False):
182         super(ModelConverter, self).__init__(url_map, model)
183         self.regex = r'(?:[A-Za-z0-9-_]+?-)?(\d+)(?=$|/)'
184
185     def to_url(self, value):
186         return slug(value)
187
188     def to_python(self, value):
189         m = re.match(self.regex, value)
190         _uid = RequestUID(value=value, match=m, converter=self)
191         return request.registry[self.model].browse(
192             request.cr, _uid, int(m.group(1)), context=request.context)
193
194     def generate(self, cr, uid, query=None, context=None):
195         return request.registry[self.model].name_search(
196             cr, uid, name=query or '', context=context)
197
198 class PageConverter(werkzeug.routing.PathConverter):
199     """ Only point of this converter is to bundle pages enumeration logic
200
201     Sads got: no way to get the view's human-readable name even if one exists
202     """
203     def generate(self, cr, uid, query=None, context=None):
204         View = request.registry['ir.ui.view']
205         views = View.search_read(
206             cr, uid, [['page', '=', True]],
207             fields=[], order='name', context=context)
208         xids = View.get_external_id(
209             cr, uid, [view['id'] for view in views], context=context)
210
211         for view in views:
212             xid = xids[view['id']]
213             if xid and (not query or query.lower() in xid.lower()):
214                 yield xid