Commit 540b753bf8 introduced
support for resources stored as ir.attachment records in
asset bundles too.
This is specifically useful for customizations.
However the HTTP route for reaching those resources
when they are *not* in a bundle was originally created
in the `website` module (as a special handling for
404 requests)
This means that these dynamic resources would only
be partially supported when `website` is not installed,
causing various problems:
- missing resources in debug mode where bundles are skipped
- errors when trying to define new client-side Qweb templates
via XML resources - which are loaded with a direct request
- ...
This commit moves back the supporting code to the web module.
The `mimetype` column is not present in ir.attachment without
the `website` module, but sniffing it based on the attachment
name works fine at serving time too.
Closes #6002
271 lines
12 KiB
Python
271 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
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, url_for, _UNSLUG_RE
|
|
from openerp.http import request
|
|
from openerp.tools import config
|
|
from openerp.osv import orm
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class RequestUID(object):
|
|
def __init__(self, **kw):
|
|
self.__dict__.update(kw)
|
|
|
|
class ir_http(orm.AbstractModel):
|
|
_inherit = 'ir.http'
|
|
|
|
rerouting_limit = 10
|
|
geo_ip_resolver = None
|
|
|
|
def _get_converters(self):
|
|
return dict(
|
|
super(ir_http, self)._get_converters(),
|
|
model=ModelConverter,
|
|
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
|
|
func = None
|
|
try:
|
|
func, arguments = self._find_handler()
|
|
request.website_enabled = func.routing.get('website', False)
|
|
except werkzeug.exceptions.NotFound:
|
|
# either we have a language prefixed route, either a real 404
|
|
# 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:
|
|
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)
|
|
langs = [lg[0] for lg in request.website.get_languages()]
|
|
path = request.httprequest.path.split('/')
|
|
if first_pass:
|
|
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
|
|
if not func:
|
|
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)
|
|
# 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.httprequest.path]
|
|
if path in request.rerouting:
|
|
raise Exception("Rerouting loop is forbidden")
|
|
request.rerouting.append(path)
|
|
if len(request.rerouting) > self.rerouting_limit:
|
|
raise Exception("Rerouting limit exceeded")
|
|
request.httprequest.environ['PATH_INFO'] = path
|
|
# void werkzeug cached_property. TODO: find a proper way to do this
|
|
for key in ('path', 'full_path', 'url', 'base_url'):
|
|
request.httprequest.__dict__.pop(key, None)
|
|
|
|
return self._dispatch()
|
|
|
|
def _postprocess_args(self, arguments, rule):
|
|
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.BaseModel) and isinstance(val._uid, RequestUID):
|
|
arguments[key] = val.sudo(request.uid)
|
|
|
|
try:
|
|
_, path = rule.build(arguments)
|
|
assert path is not None
|
|
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 _handle_exception(self, exception, code=500):
|
|
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:
|
|
exception = e
|
|
|
|
values = dict(
|
|
exception=exception,
|
|
traceback=traceback.format_exc(exception),
|
|
)
|
|
code = getattr(exception, 'code', 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 isinstance(exception, werkzeug.exceptions.HTTPException) and code is None:
|
|
# Hand-crafted HTTPException likely coming from abort(),
|
|
# usually for a redirect response -> return it directly
|
|
return exception
|
|
|
|
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 and not v.page]
|
|
values['views'] = to_reset
|
|
elif code == 403:
|
|
logger.warn("403 Forbidden:\n\n%s", values['traceback'])
|
|
|
|
values.update(
|
|
status_message=werkzeug.http.HTTP_STATUS_CODES[code],
|
|
status_code=code,
|
|
)
|
|
|
|
if not request.uid:
|
|
self._auth_method_public()
|
|
|
|
try:
|
|
html = request.website._render('website.%s' % code, values)
|
|
except Exception:
|
|
html = request.website._render('website.http_error', values)
|
|
return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8')
|
|
|
|
class ModelConverter(ir.ir_http.ModelConverter):
|
|
def __init__(self, url_map, model=False, domain='[]'):
|
|
super(ModelConverter, self).__init__(url_map, model)
|
|
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, record_id, context=request.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 """
|
|
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=['xml_id','priority','write_date'], order='name', context=context)
|
|
for view in views:
|
|
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
|