diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py index 0129bc90e6d..3388aabeb80 100644 --- a/addons/http_routing/models/ir_http.py +++ b/addons/http_routing/models/ir_http.py @@ -164,7 +164,7 @@ def is_multilang_url(local_url, langs=None): local_url = '/'.join(spath) try: # Try to match an endpoint in werkzeug's routing table - url = local_url.split('?') + url = local_url.partition('#')[0].split('?') path = url[0] query_string = url[1] if len(url) > 1 else None router = request.httprequest.app.get_db_router(request.db).bind('') @@ -173,6 +173,10 @@ def is_multilang_url(local_url, langs=None): return (func.routing.get('website', False) and func.routing.get('multilang', func.routing['type'] == 'http')) except werkzeug.exceptions.NotFound: + # Consider /static/ files as non-multilang + static_index = path.find('/static/', 1) + if static_index != -1 and static_index == path.find('/', 1): + return False return True except Exception as e: return False diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index 0f73bc469f5..0c7900e9a47 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -4,6 +4,7 @@ import re from collections import OrderedDict from odoo import models +from odoo.addons.http_routing.models.ir_http import url_for from odoo.http import request @@ -37,10 +38,16 @@ class QWeb(models.AbstractModel): if not website and options.get('website_id'): website = self.env['website'].browse(options['website_id']) - if not website or not website.cdn_activated: + if not website: return atts name = self.URL_ATTRS.get(tagName) + if request and name and name in atts: + atts[name] = url_for(atts[name]) + + if not website.cdn_activated: + return atts + if name and name in atts: atts = OrderedDict(atts) atts[name] = website.get_cdn_url(atts[name]) diff --git a/addons/website/tests/test_qweb.py b/addons/website/tests/test_qweb.py index e73083a66b2..5df4898487e 100644 --- a/addons/website/tests/test_qweb.py +++ b/addons/website/tests/test_qweb.py @@ -2,7 +2,9 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import re +import werkzeug +import odoo from odoo import tools from odoo.modules.module import get_module_resource from odoo.tests.common import TransactionCase @@ -63,3 +65,123 @@ class TestQweb(TransactionCase): "css": attachments[1].url, "user_id": demo.id, }).encode('utf8')) + +class MockObject(object): + _log_call = [] + def __init__(self, *args, **kwargs): + self.__dict__ = kwargs + def __call__(self, *args, **kwargs): + self._log_call.append((args, kwargs)) + return self + def __getitem__(self, index): + return self + +def werkzeugRaiseNotFound(*args, **kwargs): + raise werkzeug.exceptions.NotFound() + +class MockRequest(object): + """ Class with context manager mocking odoo.http.request for tests """ + def __init__(self, env, website=None, context=None, multilang=True, routing=True): + app = MockObject(routing={ + 'type': 'http', + 'website': True, + 'multilang': multilang, + }) + app.get_db_router = app.bind = app.match = app + if not routing: + app.match = werkzeugRaiseNotFound + self.request = MockObject( + env=env, context=context or {}, db=None, debug=False, + website=website, httprequest=MockObject( + path='/hello/', + app=app + ) + ) + odoo.http._request_stack.push(self.request) + def __enter__(self): + return self.request + def __exit__(self, exc_type, exc_value, traceback): + odoo.http._request_stack.pop() + +class TestQwebProcessAtt(TransactionCase): + def setUp(self): + super(TestQwebProcessAtt, self).setUp() + self.website = self.env['website'].browse(1) + self.website.language_ids = self.env.ref('base.lang_en') + self.env.ref('base.lang_fr') + self.website.default_lang_id = self.env.ref('base.lang_en') + self.website.cdn_activated = True + self.website.cdn_url = "http://test.cdn" + self.website.cdn_filters = "\n".join(["^(/[a-z]{2}_[A-Z]{2})?/a$", "^/b$"]) + + def _test_att(self, url, expect, tag='a', attribute='href'): + self.assertEqual( + self.env['ir.qweb']._post_processing_att(tag, {attribute: url}, {}), + expect + ) + + def test_process_att_no_request(self): + # no request so no URL rewriting + self._test_att('/', {'href': '/'}) + self._test_att('/en_US/', {'href': '/en_US/'}) + self._test_att('/fr_FR/', {'href': '/fr_FR/'}) + # no URL rewritting for CDN + self._test_att('/a', {'href': '/a'}) + + def test_process_att_no_website(self): + with MockRequest(self.env) as request: + # no website so URL rewriting + self._test_att('/', {'href': '/'}) + self._test_att('/en_US/', {'href': '/en_US/'}) + self._test_att('/fr_FR/', {'href': '/fr_FR/'}) + # no URL rewritting for CDN + self._test_att('/a', {'href': '/a'}) + + def test_process_att_monolang_route(self): + with MockRequest(self.env, website=self.website, multilang=False) as request: + # lang not changed in URL but CDN enabled + self._test_att('/a', {'href': 'http://test.cdn/a'}) + self._test_att('/en_US/a', {'href': 'http://test.cdn/en_US/a'}) + self._test_att('/b', {'href': 'http://test.cdn/b'}) + self._test_att('/en_US/b', {'href': '/en_US/b'}) + + def test_process_att_no_request_lang(self): + with MockRequest(self.env, self.website) as request: + self._test_att('/', {'href': '/'}) + self._test_att('/en_US/', {'href': '/'}) + self._test_att('/fr_FR/', {'href': '/fr_FR/'}) + + def test_process_att_with_request_lang(self): + with MockRequest(self.env, self.website, context={'lang': 'fr_FR'}) as request: + self._test_att('/', {'href': '/fr_FR/'}) + self._test_att('/en_US/', {'href': '/'}) + self._test_att('/fr_FR/', {'href': '/fr_FR/'}) + + def test_process_att_matching_cdn_and_lang(self): + with MockRequest(self.env, self.website) as request: + # lang prefix is added before CDN + self._test_att('/a', {'href': 'http://test.cdn/a'}) + self._test_att('/en_US/a', {'href': 'http://test.cdn/a'}) + self._test_att('/fr_FR/a', {'href': 'http://test.cdn/fr_FR/a'}) + self._test_att('/b', {'href': 'http://test.cdn/b'}) + self._test_att('/en_US/b', {'href': 'http://test.cdn/b'}) + self._test_att('/fr_FR/b', {'href': '/fr_FR/b'}) + + def test_process_att_no_route(self): + with MockRequest(self.env, self.website, context={'lang': 'fr_FR'}, routing=False) as request: + # default on multilang=True if route is not /{module}/static/ + self._test_att('/web/static/hi', {'href': '/web/static/hi'}) + self._test_att('/my-page', {'href': '/fr_FR/my-page'}) + + def test_process_att_url_crap(self): + with MockRequest(self.env, self.website) as request: + # #{fragment} is stripped from URL when testing route + self._test_att('/x#y?z', {'href': '/x#y?z'}) + self.assertEqual( + request.httprequest.app._log_call[-1], + (('/x',), {'method': 'POST', 'query_args': None}) + ) + self._test_att('/x?y#z', {'href': '/x?y#z'}) + self.assertEqual( + request.httprequest.app._log_call[-1], + (('/x',), {'method': 'POST', 'query_args': 'y'}) + )