1 # -*- coding: utf-8 -*-
10 import werkzeug.routing
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
21 logger = logging.getLogger(__name__)
23 class RequestUID(object):
24 def __init__(self, **kw):
25 self.__dict__.update(kw)
27 class ir_http(orm.AbstractModel):
31 geo_ip_resolver = None
33 def _get_converters(self):
35 super(ir_http, self)._get_converters(),
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')
45 request.uid = request.session.uid
48 first_pass = not hasattr(request, 'website')
49 request.website = None
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
59 request.website_multilang = request.website_enabled and func and func.routing.get('multilang', True)
61 if 'geoip' not in request.session:
63 if self.geo_ip_resolver is None:
66 # updated database can be downloaded on MaxMind website
67 # http://dev.maxmind.com/geoip/legacy/install/city/
68 geofile = config.get('geoip_database', '/usr/share/GeoIP/GeoLiteCity.dat')
69 if os.path.exists(geofile):
70 self.geo_ip_resolver = GeoIP.open(geofile, GeoIP.GEOIP_STANDARD)
72 self.geo_ip_resolver = False
73 logger.warning('GeoIP database file %r does not exists', geofile)
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
80 if request.website_enabled:
82 self._authenticate(func.routing['auth'])
84 self._auth_method_public()
85 request.redirect = lambda url: werkzeug.utils.redirect(url_for(url))
86 request.website = request.registry['website'].get_current_website(request.cr, request.uid, context=request.context)
88 request.lang = request.website.default_lang_code
89 request.context['lang'] = request.lang
91 path = request.httprequest.path.split('/')
92 langs = [lg[0] for lg in request.website.get_languages()]
94 request.lang = request.context['lang'] = path.pop(1)
95 path = '/'.join(path) or '/'
96 if request.lang == request.website.default_lang_code:
97 # If language is in the url and it is the default language, redirect
98 # to url without language so google doesn't see duplicate content
99 return request.redirect(path + '?' + request.httprequest.query_string)
100 return self.reroute(path)
101 return super(ir_http, self)._dispatch()
103 def reroute(self, path):
104 if not hasattr(request, 'rerouting'):
105 request.rerouting = [request.httprequest.path]
106 if path in request.rerouting:
107 raise Exception("Rerouting loop is forbidden")
108 request.rerouting.append(path)
109 if len(request.rerouting) > self.rerouting_limit:
110 raise Exception("Rerouting limit exceeded")
111 request.httprequest.environ['PATH_INFO'] = path
112 # void werkzeug cached_property. TODO: find a proper way to do this
113 for key in ('path', 'full_path', 'url', 'base_url'):
114 request.httprequest.__dict__.pop(key, None)
116 return self._dispatch()
118 def _postprocess_args(self, arguments, rule):
119 super(ir_http, self)._postprocess_args(arguments, rule)
121 for key, val in arguments.items():
122 # Replace uid placeholder by the current request.uid
123 if isinstance(val, orm.BaseModel) and isinstance(val._uid, RequestUID):
124 arguments[key] = val.sudo(request.uid)
127 _, path = rule.build(arguments)
128 assert path is not None
130 return self._handle_exception(e, code=404)
132 if getattr(request, 'website_multilang', False) and request.httprequest.method in ('GET', 'HEAD'):
133 generated_path = werkzeug.url_unquote_plus(path)
134 current_path = werkzeug.url_unquote_plus(request.httprequest.path)
135 if generated_path != current_path:
136 if request.lang != request.website.default_lang_code:
137 path = '/' + request.lang + path
138 if request.httprequest.query_string:
139 path += '?' + request.httprequest.query_string
140 return werkzeug.utils.redirect(path)
142 def _serve_attachment(self):
143 domain = [('type', '=', 'binary'), ('url', '=', request.httprequest.path)]
144 attach = self.pool['ir.attachment'].search_read(request.cr, openerp.SUPERUSER_ID, domain, ['__last_update', 'datas', 'mimetype'], context=request.context)
146 wdate = attach[0]['__last_update']
147 datas = attach[0]['datas']
148 response = werkzeug.wrappers.Response()
149 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
151 response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f')
153 # just in case we have a timestamp without microseconds
154 response.last_modified = datetime.datetime.strptime(wdate, server_format)
156 response.set_etag(hashlib.sha1(datas).hexdigest())
157 response.make_conditional(request.httprequest)
159 if response.status_code == 304:
162 response.mimetype = attach[0]['mimetype'] or 'application/octet-stream'
163 response.data = datas.decode('base64')
166 def _handle_exception(self, exception, code=500):
167 # This is done first as the attachment path may
168 # not match any HTTP controller, so the request
169 # may not be website-enabled.
170 attach = self._serve_attachment()
174 is_website_request = bool(getattr(request, 'website_enabled', False) and request.website)
175 if not is_website_request:
176 # Don't touch non website requests exception handling
177 return super(ir_http, self)._handle_exception(exception)
180 response = super(ir_http, self)._handle_exception(exception)
181 if isinstance(response, Exception):
184 # if parent excplicitely returns a plain response, then we don't touch it
191 traceback=traceback.format_exc(exception),
193 code = getattr(exception, 'code', code)
195 if isinstance(exception, openerp.exceptions.AccessError):
198 if isinstance(exception, ir_qweb.QWebException):
199 values.update(qweb_exception=exception)
200 if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
203 if isinstance(exception, werkzeug.exceptions.HTTPException) and code is None:
204 # Hand-crafted HTTPException likely coming from abort(),
205 # usually for a redirect response -> return it directly
209 logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
210 if 'qweb_exception' in values:
211 view = request.registry.get("ir.ui.view")
212 views = view._views_get(request.cr, request.uid, exception.qweb['template'], request.context)
213 to_reset = [v for v in views if v.model_data_id.noupdate is True and not v.page]
214 values['views'] = to_reset
216 logger.warn("403 Forbidden:\n\n%s", values['traceback'])
219 status_message=werkzeug.http.HTTP_STATUS_CODES[code],
224 self._auth_method_public()
227 html = request.website._render('website.%s' % code, values)
229 html = request.website._render('website.http_error', values)
230 return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
232 class ModelConverter(ir.ir_http.ModelConverter):
233 def __init__(self, url_map, model=False, domain='[]'):
234 super(ModelConverter, self).__init__(url_map, model)
236 self.regex = _UNSLUG_RE.pattern
238 def to_url(self, value):
241 def to_python(self, value):
242 m = re.match(self.regex, value)
243 _uid = RequestUID(value=value, match=m, converter=self)
244 return request.registry[self.model].browse(
245 request.cr, _uid, int(m.group(2)), context=request.context)
247 def generate(self, cr, uid, query=None, args=None, context=None):
248 obj = request.registry[self.model]
249 domain = eval( self.domain, (args or {}).copy())
251 domain.append((obj._rec_name, 'ilike', '%'+query+'%'))
252 for record in obj.search_read(cr, uid, domain=domain, fields=['write_date',obj._rec_name], context=context):
253 if record.get(obj._rec_name, False):
254 yield {'loc': (record['id'], record[obj._rec_name])}
256 class PageConverter(werkzeug.routing.PathConverter):
257 """ Only point of this converter is to bundle pages enumeration logic """
258 def generate(self, cr, uid, query=None, args={}, context=None):
259 View = request.registry['ir.ui.view']
260 views = View.search_read(cr, uid, [['page', '=', True]],
261 fields=['xml_id','priority','write_date'], order='name', context=context)
263 xid = view['xml_id'].startswith('website.') and view['xml_id'][8:] or view['xml_id']
264 # the 'page/homepage' url is indexed as '/', avoid aving the same page referenced twice
265 # when we will have an url mapping mechanism, replace this by a rule: page/homepage --> /
266 if xid=='homepage': continue
267 if query and query.lower() not in xid.lower():
269 record = {'loc': xid}
270 if view['priority'] <> 16:
271 record['__priority'] = min(round(view['priority'] / 32.0,1), 1)
272 if view['write_date']:
273 record['__lastmod'] = view['write_date'][:10]