[IMP] ir_http: don't handle exception in dev mode but use the werkzeug debugger excep...
[odoo/odoo.git] / addons / website / models / ir_http.py
1 # -*- coding: utf-8 -*-
2 import datetime
3 import hashlib
4 import logging
5 import os
6 import re
7 import traceback
8
9 import werkzeug
10 import werkzeug.routing
11 import werkzeug.utils
12
13 import openerp
14 from openerp.addons.base import ir
15 from openerp.addons.base.ir import ir_qweb
16 from openerp.addons.website.models.website import slug, url_for, _UNSLUG_RE
17 from openerp.http import request
18 from openerp.tools import config
19 from openerp.osv import orm
20
21 logger = logging.getLogger(__name__)
22
23 class RequestUID(object):
24     def __init__(self, **kw):
25         self.__dict__.update(kw)
26
27 class ir_http(orm.AbstractModel):
28     _inherit = 'ir.http'
29
30     rerouting_limit = 10
31     geo_ip_resolver = None
32
33     def _get_converters(self):
34         return dict(
35             super(ir_http, self)._get_converters(),
36             model=ModelConverter,
37             page=PageConverter,
38         )
39
40     def _auth_method_public(self):
41         # TODO: select user_id from matching website
42         if not request.session.uid:
43             request.uid = self.pool['ir.model.data'].xmlid_to_res_id(request.cr, openerp.SUPERUSER_ID, 'base.public_user')
44         else:
45             request.uid = request.session.uid
46
47     def _dispatch(self):
48         first_pass = not hasattr(request, 'website')
49         request.website = None
50         func = None
51         try:
52             func, arguments = self._find_handler()
53             request.website_enabled = func.routing.get('website', False)
54         except werkzeug.exceptions.NotFound:
55             # either we have a language prefixed route, either a real 404
56             # in all cases, website processes them
57             request.website_enabled = True
58
59         request.website_multilang = request.website_enabled and func and func.routing.get('multilang', True)
60
61         if 'geoip' not in request.session:
62             record = {}
63             if self.geo_ip_resolver is None:
64                 try:
65                     import GeoIP
66                     # updated database can be downloaded on MaxMind website
67                     # http://dev.maxmind.com/geoip/legacy/install/city/
68                     geofile = config.get('geoip_database')
69                     if os.path.exists(geofile):
70                         self.geo_ip_resolver = GeoIP.open(geofile, GeoIP.GEOIP_STANDARD)
71                     else:
72                         self.geo_ip_resolver = False
73                         logger.warning('GeoIP database file %r does not exists', geofile)
74                 except ImportError:
75                     self.geo_ip_resolver = False
76             if self.geo_ip_resolver and request.httprequest.remote_addr:
77                 record = self.geo_ip_resolver.record_by_addr(request.httprequest.remote_addr) or {}
78             request.session['geoip'] = record
79             
80         if request.website_enabled:
81             try:
82                 if func:
83                     self._authenticate(func.routing['auth'])
84                 else:
85                     self._auth_method_public()
86             except Exception as e:
87                 return self._handle_exception(e)
88
89             request.redirect = lambda url, code=302: werkzeug.utils.redirect(url_for(url), code)
90             request.website = request.registry['website'].get_current_website(request.cr, request.uid, context=request.context)
91             request.context['website_id'] = request.website.id
92             langs = [lg[0] for lg in request.website.get_languages()]
93             path = request.httprequest.path.split('/')
94             if first_pass:
95                 if request.website_multilang:
96                     # If the url doesn't contains the lang and that it's the first connection, we to retreive the user preference if it exists.
97                     if not path[1] in langs and not request.httprequest.cookies.get('session_id'):
98                         if request.lang not in langs:
99                             # Try to find a similar lang. Eg: fr_BE and fr_FR
100                             short = request.lang.split('_')[0]
101                             langs_withshort = [lg[0] for lg in request.website.get_languages() if lg[0].startswith(short)]
102                             if len(langs_withshort):
103                                 request.lang = langs_withshort[0]
104                             else:
105                                 request.lang = request.website.default_lang_code
106                         # We redirect with the right language in url
107                         if request.lang != request.website.default_lang_code:
108                             path.insert(1, request.lang)
109                             path = '/'.join(path) or '/'
110                             return request.redirect(path + '?' + request.httprequest.query_string)
111                     else:
112                         request.lang = request.website.default_lang_code
113
114             request.context['lang'] = request.lang
115             if not request.context.get('tz'):
116                 request.context['tz'] = request.session['geoip'].get('time_zone')
117             if not func:
118                 if path[1] in langs:
119                     request.lang = request.context['lang'] = path.pop(1)
120                     path = '/'.join(path) or '/'
121                     if request.lang == request.website.default_lang_code:
122                         # If language is in the url and it is the default language, redirect
123                         # to url without language so google doesn't see duplicate content
124                         return request.redirect(path + '?' + request.httprequest.query_string, code=301)
125                     return self.reroute(path)
126             # bind modified context
127             request.website = request.website.with_context(request.context)
128         return super(ir_http, self)._dispatch()
129
130     def reroute(self, path):
131         if not hasattr(request, 'rerouting'):
132             request.rerouting = [request.httprequest.path]
133         if path in request.rerouting:
134             raise Exception("Rerouting loop is forbidden")
135         request.rerouting.append(path)
136         if len(request.rerouting) > self.rerouting_limit:
137             raise Exception("Rerouting limit exceeded")
138         request.httprequest.environ['PATH_INFO'] = path
139         # void werkzeug cached_property. TODO: find a proper way to do this
140         for key in ('path', 'full_path', 'url', 'base_url'):
141             request.httprequest.__dict__.pop(key, None)
142
143         return self._dispatch()
144
145     def _postprocess_args(self, arguments, rule):
146         super(ir_http, self)._postprocess_args(arguments, rule)
147
148         for key, val in arguments.items():
149             # Replace uid placeholder by the current request.uid
150             if isinstance(val, orm.BaseModel) and isinstance(val._uid, RequestUID):
151                 arguments[key] = val.sudo(request.uid)
152
153         try:
154             _, path = rule.build(arguments)
155             assert path is not None
156         except Exception, e:
157             return self._handle_exception(e, code=404)
158
159         if getattr(request, 'website_multilang', False) and request.httprequest.method in ('GET', 'HEAD'):
160             generated_path = werkzeug.url_unquote_plus(path)
161             current_path = werkzeug.url_unquote_plus(request.httprequest.path)
162             if generated_path != current_path:
163                 if request.lang != request.website.default_lang_code:
164                     path = '/' + request.lang + path
165                 if request.httprequest.query_string:
166                     path += '?' + request.httprequest.query_string
167                 return werkzeug.utils.redirect(path, code=301)
168
169     def _serve_attachment(self):
170         domain = [('type', '=', 'binary'), ('url', '=', request.httprequest.path)]
171         attach = self.pool['ir.attachment'].search_read(request.cr, openerp.SUPERUSER_ID, domain, ['__last_update', 'datas', 'mimetype'], context=request.context)
172         if attach:
173             wdate = attach[0]['__last_update']
174             datas = attach[0]['datas']
175             response = werkzeug.wrappers.Response()
176             server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
177             try:
178                 response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f')
179             except ValueError:
180                 # just in case we have a timestamp without microseconds
181                 response.last_modified = datetime.datetime.strptime(wdate, server_format)
182
183             response.set_etag(hashlib.sha1(datas).hexdigest())
184             response.make_conditional(request.httprequest)
185
186             if response.status_code == 304:
187                 return response
188
189             response.mimetype = attach[0]['mimetype'] or 'application/octet-stream'
190             response.data = datas.decode('base64')
191             return response
192
193     def _handle_exception(self, exception, code=500):
194         # This is done first as the attachment path may
195         # not match any HTTP controller, so the request
196         # may not be website-enabled.
197         attach = self._serve_attachment()
198         if attach:
199             return attach
200
201         is_website_request = bool(getattr(request, 'website_enabled', False) and request.website)
202         if not is_website_request:
203             # Don't touch non website requests exception handling
204             return super(ir_http, self)._handle_exception(exception)
205         else:
206             try:
207                 response = super(ir_http, self)._handle_exception(exception)
208                 if isinstance(response, Exception):
209                     exception = response
210                 else:
211                     # if parent excplicitely returns a plain response, then we don't touch it
212                     return response
213             except Exception, e:
214                 if openerp.tools.config['dev_mode'] and (not isinstance(exception, ir_qweb.QWebException) or not exception.qweb.get('cause')):
215                     raise
216                 exception = e
217
218             values = dict(
219                 exception=exception,
220                 traceback=traceback.format_exc(exception),
221             )
222
223             if isinstance(exception, werkzeug.exceptions.HTTPException):
224                 if exception.code is None:
225                     # Hand-crafted HTTPException likely coming from abort(),
226                     # usually for a redirect response -> return it directly
227                     return exception
228                 else:
229                     code = exception.code
230
231             if isinstance(exception, openerp.exceptions.AccessError):
232                 code = 403
233
234             if isinstance(exception, ir_qweb.QWebException):
235                 values.update(qweb_exception=exception)
236                 if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
237                     code = 403
238
239             if code == 500:
240                 logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
241                 if 'qweb_exception' in values:
242                     view = request.registry.get("ir.ui.view")
243                     views = view._views_get(request.cr, request.uid, exception.qweb['template'], request.context)
244                     to_reset = [v for v in views if v.model_data_id.noupdate is True and not v.page]
245                     values['views'] = to_reset
246             elif code == 403:
247                 logger.warn("403 Forbidden:\n\n%s", values['traceback'])
248
249             values.update(
250                 status_message=werkzeug.http.HTTP_STATUS_CODES[code],
251                 status_code=code,
252             )
253
254             if not request.uid:
255                 self._auth_method_public()
256
257             try:
258                 html = request.website._render('website.%s' % code, values)
259             except Exception:
260                 html = request.website._render('website.http_error', values)
261             return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
262
263 class ModelConverter(ir.ir_http.ModelConverter):
264     def __init__(self, url_map, model=False, domain='[]'):
265         super(ModelConverter, self).__init__(url_map, model)
266         self.domain = domain
267         self.regex = _UNSLUG_RE.pattern
268
269     def to_url(self, value):
270         return slug(value)
271
272     def to_python(self, value):
273         m = re.match(self.regex, value)
274         _uid = RequestUID(value=value, match=m, converter=self)
275         record_id = int(m.group(2))
276         if record_id < 0:
277             # limited support for negative IDs due to our slug pattern, assume abs() if not found
278             if not request.registry[self.model].exists(request.cr, _uid, [record_id]):
279                 record_id = abs(record_id)
280         return request.registry[self.model].browse(
281             request.cr, _uid, record_id, context=request.context)
282
283     def generate(self, cr, uid, query=None, args=None, context=None):
284         obj = request.registry[self.model]
285         domain = eval( self.domain, (args or {}).copy())
286         if query:
287             domain.append((obj._rec_name, 'ilike', '%'+query+'%'))
288         for record in obj.search_read(cr, uid, domain=domain, fields=['write_date',obj._rec_name], context=context):
289             if record.get(obj._rec_name, False):
290                 yield {'loc': (record['id'], record[obj._rec_name])}
291
292 class PageConverter(werkzeug.routing.PathConverter):
293     """ Only point of this converter is to bundle pages enumeration logic """
294     def generate(self, cr, uid, query=None, args={}, context=None):
295         View = request.registry['ir.ui.view']
296         views = View.search_read(cr, uid, [['page', '=', True]],
297             fields=['xml_id','priority','write_date'], order='name', context=context)
298         for view in views:
299             xid = view['xml_id'].startswith('website.') and view['xml_id'][8:] or view['xml_id']
300             # the 'page/homepage' url is indexed as '/', avoid aving the same page referenced twice
301             # when we will have an url mapping mechanism, replace this by a rule: page/homepage --> /
302             if xid=='homepage': continue
303             if query and query.lower() not in xid.lower():
304                 continue
305             record = {'loc': xid}
306             if view['priority'] <> 16:
307                 record['__priority'] = min(round(view['priority'] / 32.0,1), 1)
308             if view['write_date']:
309                 record['__lastmod'] = view['write_date'][:10]
310             yield record