# -*- coding: utf-8 -*-
+import datetime
+import hashlib
import logging
+import os
import re
import traceback
import werkzeug
import werkzeug.routing
+import werkzeug.utils
import openerp
from openerp.addons.base import ir
from openerp.addons.base.ir import ir_qweb
-from openerp.addons.website.models.website import slug
+from openerp.addons.website.models.website import slug, url_for, _UNSLUG_RE
from openerp.http import request
+from openerp.tools import config
from openerp.osv import orm
logger = logging.getLogger(__name__)
_inherit = 'ir.http'
rerouting_limit = 10
+ geo_ip_resolver = None
def _get_converters(self):
return dict(
page=PageConverter,
)
+ def _auth_method_public(self):
+ # TODO: select user_id from matching website
+ if not request.session.uid:
+ request.uid = self.pool['ir.model.data'].xmlid_to_res_id(request.cr, openerp.SUPERUSER_ID, 'base.public_user')
+ else:
+ request.uid = request.session.uid
+
def _dispatch(self):
first_pass = not hasattr(request, 'website')
request.website = None
# in all cases, website processes them
request.website_enabled = True
+ request.website_multilang = request.website_enabled and func and func.routing.get('multilang', True)
+
+ if 'geoip' not in request.session:
+ record = {}
+ if self.geo_ip_resolver is None:
+ try:
+ import GeoIP
+ # updated database can be downloaded on MaxMind website
+ # http://dev.maxmind.com/geoip/legacy/install/city/
+ geofile = config.get('geoip_database')
+ if os.path.exists(geofile):
+ self.geo_ip_resolver = GeoIP.open(geofile, GeoIP.GEOIP_STANDARD)
+ else:
+ self.geo_ip_resolver = False
+ logger.warning('GeoIP database file %r does not exists', geofile)
+ except ImportError:
+ self.geo_ip_resolver = False
+ if self.geo_ip_resolver and request.httprequest.remote_addr:
+ record = self.geo_ip_resolver.record_by_addr(request.httprequest.remote_addr) or {}
+ request.session['geoip'] = record
+
if request.website_enabled:
- if func:
- self._authenticate(func.routing['auth'])
- else:
- self._auth_method_public()
+ try:
+ if func:
+ self._authenticate(func.routing['auth'])
+ else:
+ self._auth_method_public()
+ except Exception as e:
+ return self._handle_exception(e)
+
+ request.redirect = lambda url, code=302: werkzeug.utils.redirect(url_for(url), code)
request.website = request.registry['website'].get_current_website(request.cr, request.uid, context=request.context)
+ request.context['website_id'] = request.website.id
+ langs = [lg[0] for lg in request.website.get_languages()]
+ path = request.httprequest.path.split('/')
if first_pass:
- request.lang = request.website.default_lang_code
+ if request.website_multilang:
+ # If the url doesn't contains the lang and that it's the first connection, we to retreive the user preference if it exists.
+ if not path[1] in langs and not request.httprequest.cookies.get('session_id'):
+ if request.lang not in langs:
+ # Try to find a similar lang. Eg: fr_BE and fr_FR
+ short = request.lang.split('_')[0]
+ langs_withshort = [lg[0] for lg in request.website.get_languages() if lg[0].startswith(short)]
+ if len(langs_withshort):
+ request.lang = langs_withshort[0]
+ else:
+ request.lang = request.website.default_lang_code
+ # We redirect with the right language in url
+ if request.lang != request.website.default_lang_code:
+ path.insert(1, request.lang)
+ path = '/'.join(path) or '/'
+ return request.redirect(path + '?' + request.httprequest.query_string)
+ else:
+ request.lang = request.website.default_lang_code
+
request.context['lang'] = request.lang
- request.website.preprocess_request(request)
+ if not request.context.get('tz'):
+ request.context['tz'] = request.session['geoip'].get('time_zone')
if not func:
- path = request.httprequest.path.split('/')
- langs = [lg[0] for lg in request.website.get_languages()]
if path[1] in langs:
request.lang = request.context['lang'] = path.pop(1)
path = '/'.join(path) or '/'
+ if request.lang == request.website.default_lang_code:
+ # If language is in the url and it is the default language, redirect
+ # to url without language so google doesn't see duplicate content
+ return request.redirect(path + '?' + request.httprequest.query_string, code=301)
return self.reroute(path)
- return self._handle_exception(code=404)
+ # bind modified context
+ request.website = request.website.with_context(request.context)
return super(ir_http, self)._dispatch()
def reroute(self, path):
if not hasattr(request, 'rerouting'):
- request.rerouting = []
+ request.rerouting = [request.httprequest.path]
if path in request.rerouting:
raise Exception("Rerouting loop is forbidden")
request.rerouting.append(path)
return self._dispatch()
def _postprocess_args(self, arguments, rule):
- for arg, val in arguments.items():
+ super(ir_http, self)._postprocess_args(arguments, rule)
+
+ for key, val in arguments.items():
# Replace uid placeholder by the current request.uid
- if isinstance(val, orm.browse_record) and isinstance(val._uid, RequestUID):
- val._uid = request.uid
+ if isinstance(val, orm.BaseModel) and isinstance(val._uid, RequestUID):
+ arguments[key] = val.sudo(request.uid)
+
try:
_, path = rule.build(arguments)
assert path is not None
- except Exception:
- return self._handle_exception(werkzeug.exceptions.NotFound())
- if path != request.httprequest.path:
- return werkzeug.utils.redirect(path)
-
- def _handle_exception(self, exception=None, code=500):
- if isinstance(exception, werkzeug.exceptions.HTTPException) and hasattr(exception, 'response') and exception.response:
- return exception.response
- if getattr(request, 'website_enabled', False) and request.website:
+ except Exception, e:
+ return self._handle_exception(e, code=404)
+
+ if getattr(request, 'website_multilang', False) and request.httprequest.method in ('GET', 'HEAD'):
+ generated_path = werkzeug.url_unquote_plus(path)
+ current_path = werkzeug.url_unquote_plus(request.httprequest.path)
+ if generated_path != current_path:
+ if request.lang != request.website.default_lang_code:
+ path = '/' + request.lang + path
+ if request.httprequest.query_string:
+ path += '?' + request.httprequest.query_string
+ return werkzeug.utils.redirect(path, code=301)
+
+ def _serve_attachment(self):
+ domain = [('type', '=', 'binary'), ('url', '=', request.httprequest.path)]
+ attach = self.pool['ir.attachment'].search_read(request.cr, openerp.SUPERUSER_ID, domain, ['__last_update', 'datas', 'mimetype'], context=request.context)
+ if attach:
+ wdate = attach[0]['__last_update']
+ datas = attach[0]['datas']
+ response = werkzeug.wrappers.Response()
+ server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
+ try:
+ response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f')
+ except ValueError:
+ # just in case we have a timestamp without microseconds
+ response.last_modified = datetime.datetime.strptime(wdate, server_format)
+
+ response.set_etag(hashlib.sha1(datas).hexdigest())
+ response.make_conditional(request.httprequest)
+
+ if response.status_code == 304:
+ return response
+
+ response.mimetype = attach[0]['mimetype'] or 'application/octet-stream'
+ response.data = datas.decode('base64')
+ return response
+
+ def _handle_exception(self, exception, code=500):
+ # This is done first as the attachment path may
+ # not match any HTTP controller, so the request
+ # may not be website-enabled.
+ attach = self._serve_attachment()
+ if attach:
+ return attach
+
+ is_website_request = bool(getattr(request, 'website_enabled', False) and request.website)
+ if not is_website_request:
+ # Don't touch non website requests exception handling
+ return super(ir_http, self)._handle_exception(exception)
+ else:
+ try:
+ response = super(ir_http, self)._handle_exception(exception)
+ if isinstance(response, Exception):
+ exception = response
+ else:
+ # if parent excplicitely returns a plain response, then we don't touch it
+ return response
+ except Exception, e:
+ if openerp.tools.config['dev_mode'] and (not isinstance(exception, ir_qweb.QWebException) or not exception.qweb.get('cause')):
+ raise
+ exception = e
+
values = dict(
exception=exception,
traceback=traceback.format_exc(exception),
)
- if exception:
- code = getattr(exception, 'code', code)
- if isinstance(exception, ir_qweb.QWebException):
- values.update(qweb_exception=exception)
- if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
- code = 403
+
+ if isinstance(exception, werkzeug.exceptions.HTTPException):
+ if exception.code is None:
+ # Hand-crafted HTTPException likely coming from abort(),
+ # usually for a redirect response -> return it directly
+ return exception
+ else:
+ code = exception.code
+
+ if isinstance(exception, openerp.exceptions.AccessError):
+ code = 403
+
+ if isinstance(exception, ir_qweb.QWebException):
+ values.update(qweb_exception=exception)
+ if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
+ code = 403
+
if code == 500:
logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
if 'qweb_exception' in values:
view = request.registry.get("ir.ui.view")
views = view._views_get(request.cr, request.uid, exception.qweb['template'], request.context)
- to_reset = [v for v in views if v.model_data_id.noupdate is True]
+ to_reset = [v for v in views if v.model_data_id.noupdate is True and not v.page]
values['views'] = to_reset
elif code == 403:
logger.warn("403 Forbidden:\n\n%s", values['traceback'])
html = request.website._render('website.http_error', values)
return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
- return super(ir_http, self)._handle_exception(exception)
-
class ModelConverter(ir.ir_http.ModelConverter):
- def __init__(self, url_map, model=False):
+ def __init__(self, url_map, model=False, domain='[]'):
super(ModelConverter, self).__init__(url_map, model)
- self.regex = r'(?:[A-Za-z0-9-_]+?-)?(\d+)(?=$|/)'
+ self.domain = domain
+ self.regex = _UNSLUG_RE.pattern
def to_url(self, value):
return slug(value)
def to_python(self, value):
m = re.match(self.regex, value)
_uid = RequestUID(value=value, match=m, converter=self)
+ record_id = int(m.group(2))
+ if record_id < 0:
+ # limited support for negative IDs due to our slug pattern, assume abs() if not found
+ if not request.registry[self.model].exists(request.cr, _uid, [record_id]):
+ record_id = abs(record_id)
return request.registry[self.model].browse(
- request.cr, _uid, int(m.group(1)), context=request.context)
+ request.cr, _uid, record_id, context=request.context)
- def generate(self, cr, uid, query=None, context=None):
- return request.registry[self.model].name_search(
- cr, uid, name=query or '', context=context)
+ def generate(self, cr, uid, query=None, args=None, context=None):
+ obj = request.registry[self.model]
+ domain = eval( self.domain, (args or {}).copy())
+ if query:
+ domain.append((obj._rec_name, 'ilike', '%'+query+'%'))
+ for record in obj.search_read(cr, uid, domain=domain, fields=['write_date',obj._rec_name], context=context):
+ if record.get(obj._rec_name, False):
+ yield {'loc': (record['id'], record[obj._rec_name])}
class PageConverter(werkzeug.routing.PathConverter):
- """ Only point of this converter is to bundle pages enumeration logic
-
- Sads got: no way to get the view's human-readable name even if one exists
- """
- def generate(self, cr, uid, query=None, context=None):
+ """ Only point of this converter is to bundle pages enumeration logic """
+ def generate(self, cr, uid, query=None, args={}, context=None):
View = request.registry['ir.ui.view']
- views = View.search_read(
- cr, uid, [['page', '=', True]],
- fields=[], order='name', context=context)
- xids = View.get_external_id(
- cr, uid, [view['id'] for view in views], context=context)
-
+ views = View.search_read(cr, uid, [['page', '=', True]],
+ fields=['xml_id','priority','write_date'], order='name', context=context)
for view in views:
- xid = xids[view['id']]
- if xid and (not query or query.lower() in xid.lower()):
- yield xid
+ xid = view['xml_id'].startswith('website.') and view['xml_id'][8:] or view['xml_id']
+ # the 'page/homepage' url is indexed as '/', avoid aving the same page referenced twice
+ # when we will have an url mapping mechanism, replace this by a rule: page/homepage --> /
+ if xid=='homepage': continue
+ if query and query.lower() not in xid.lower():
+ continue
+ record = {'loc': xid}
+ if view['priority'] <> 16:
+ record['__priority'] = min(round(view['priority'] / 32.0,1), 1)
+ if view['write_date']:
+ record['__lastmod'] = view['write_date'][:10]
+ yield record