diff --git a/addons/account/tests/test_portal_attachment.py b/addons/account/tests/test_portal_attachment.py index 182c0d33a9b..584d8e8b259 100644 --- a/addons/account/tests/test_portal_attachment.py +++ b/addons/account/tests/test_portal_attachment.py @@ -10,7 +10,7 @@ from odoo.tools import mute_logger @tests.tagged('post_install', '-at_install') class TestUi(tests.HttpCase): - @mute_logger('odoo.addons.website.models.ir_http', 'odoo.http') + @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http') def test_01_portal_attachment(self): """Test the portal chatter attachment route.""" diff --git a/addons/http_routing/__manifest__.py b/addons/http_routing/__manifest__.py index f4636332f45..f93dcdf96dc 100644 --- a/addons/http_routing/__manifest__.py +++ b/addons/http_routing/__manifest__.py @@ -11,6 +11,7 @@ Proposes advanced routing options not available in web or base to keep base modules simple. """, 'data': [ + 'views/http_routing_template.xml', 'views/res_lang_views.xml', ], 'depends': ['web'], diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py index 71d62d054db..06d2ee87aa8 100644 --- a/addons/http_routing/models/ir_http.py +++ b/addons/http_routing/models/ir_http.py @@ -5,6 +5,7 @@ import json import logging import os import re +import traceback import unicodedata import werkzeug @@ -15,8 +16,9 @@ except ImportError: slugify_lib = None import odoo -from odoo import api, models +from odoo import api, models, registry, exceptions from odoo.addons.base.models.ir_http import RequestUID, ModelConverter +from odoo.addons.base.models.qweb import QWebException from odoo.http import request from odoo.osv import expression from odoo.tools import config, ustr, pycompat @@ -540,3 +542,87 @@ class IrHttp(models.AbstractModel): if request.httprequest.query_string: path += '?' + request.httprequest.query_string.decode('utf-8') return werkzeug.utils.redirect(path, code=301) + + @classmethod + def _get_exception_code_values(cls, exception): + """ Return a tuple with the error code following by the values matching the exception""" + code = 500 # default code + values = dict( + exception=exception, + traceback=traceback.format_exc(), + ) + # only except_orm exceptions contain a message + if isinstance(exception, exceptions.except_orm): + values['error_message'] = exception.name + code = 400 + if isinstance(exception, exceptions.AccessError): + code = 403 + + elif isinstance(exception, QWebException): + values.update(qweb_exception=exception) + + if type(exception.error) == exceptions.AccessError: + code = 403 + + elif isinstance(exception, werkzeug.exceptions.HTTPException): + code = exception.code + + values.update( + status_message=werkzeug.http.HTTP_STATUS_CODES[code], + status_code=code, + ) + + return (code, values) + + @classmethod + def _get_values_500_error(cls, env, values, exception): + values['view'] = env["ir.ui.view"] + return values + + @classmethod + def _get_error_html(cls, env, code, values): + return env['ir.ui.view'].render_template('http_routing.%s' % code, values) + + @classmethod + def _handle_exception(cls, exception): + is_frontend_request = bool(getattr(request, 'is_frontend', False)) + if not is_frontend_request: + # Don't touch non frontend requests exception handling + return super(IrHttp, cls)._handle_exception(exception) + try: + response = super(IrHttp, cls)._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 as e: + if 'werkzeug' in config['dev_mode']: + raise e + exception = e + + code, values = cls._get_exception_code_values(exception) + + if code is None: + # Hand-crafted HTTPException likely coming from abort(), + # usually for a redirect response -> return it directly + return exception + + if not request.uid: + cls._auth_method_public() + with registry(request.env.cr.dbname).cursor() as cr: + env = api.Environment(cr, request.uid, request.env.context) + if code == 500: + _logger.error("500 Internal Server Error:\n\n%s", values['traceback']) + values = cls._get_values_500_error(env, values, exception) + elif code == 403: + _logger.warn("403 Forbidden:\n\n%s", values['traceback']) + elif code == 400: + _logger.warn("400 Bad Request:\n\n%s", values['traceback']) + try: + html = cls._get_error_html(env, code, values) + except Exception: + html = env['ir.ui.view'].render_template('http_routing.http_error', values) + + return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8') diff --git a/addons/http_routing/views/http_routing_template.xml b/addons/http_routing/views/http_routing_template.xml new file mode 100644 index 00000000000..648b4c3afd8 --- /dev/null +++ b/addons/http_routing/views/http_routing_template.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + diff --git a/addons/test_website/tests/test_error.py b/addons/test_website/tests/test_error.py index 63dad4133f1..f4c9334ee97 100644 --- a/addons/test_website/tests/test_error.py +++ b/addons/test_website/tests/test_error.py @@ -5,6 +5,6 @@ from odoo.tools import mute_logger @odoo.tests.common.tagged('post_install', '-at_install') class TestWebsiteError(odoo.tests.HttpCase): - @mute_logger('odoo.addons.website.models.ir_http', 'odoo.http') + @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http') def test_01_run_test(self): self.start_tour("/test_error_view", 'test_error_website') diff --git a/addons/test_website/tests/test_reset_views.py b/addons/test_website/tests/test_reset_views.py index b9bf3801363..a11e82824c3 100644 --- a/addons/test_website/tests/test_reset_views.py +++ b/addons/test_website/tests/test_reset_views.py @@ -31,7 +31,7 @@ class TestWebsiteResetViews(odoo.tests.HttpCase): self.View = self.env['ir.ui.view'] self.test_view = self.Website.viewref('test_website.test_view') - @mute_logger('odoo.addons.website.models.ir_http') + @mute_logger('odoo.addons.http_routing.models.ir_http') def test_01_reset_specific_page_view(self): self.test_page_view = self.Website.viewref('test_website.test_page_view') total_views = self.View.search_count([('type', '=', 'qweb')]) @@ -40,7 +40,7 @@ class TestWebsiteResetViews(odoo.tests.HttpCase): self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view") self.fix_it('/test_page_view') - @mute_logger('odoo.addons.website.models.ir_http') + @mute_logger('odoo.addons.http_routing.models.ir_http') def test_02_reset_specific_view_controller(self): total_views = self.View.search_count([('type', '=', 'qweb')]) # Trigger COW then break the QWEB XML on it @@ -49,7 +49,7 @@ class TestWebsiteResetViews(odoo.tests.HttpCase): self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view") self.fix_it('/test_view') - @mute_logger('odoo.addons.website.models.ir_http') + @mute_logger('odoo.addons.http_routing.models.ir_http') def test_03_reset_specific_view_controller_t_called(self): self.test_view_to_be_t_called = self.Website.viewref('test_website.test_view_to_be_t_called') @@ -60,7 +60,7 @@ class TestWebsiteResetViews(odoo.tests.HttpCase): self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view") self.fix_it('/test_view') - @mute_logger('odoo.addons.website.models.ir_http') + @mute_logger('odoo.addons.http_routing.models.ir_http') def test_04_reset_specific_view_controller_inherit(self): self.test_view_child_broken = self.Website.viewref('test_website.test_view_child_broken') @@ -71,7 +71,7 @@ class TestWebsiteResetViews(odoo.tests.HttpCase): self.fix_it('/test_view') # This test work in real life, but not in test mode since we cannot rollback savepoint. - # @mute_logger('odoo.addons.website.models.ir_http', 'odoo.addons.website.models.ir_ui_view') + # @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.addons.website.models.ir_ui_view') # def test_05_reset_specific_view_controller_broken_request(self): # total_views = self.View.search_count([('type', '=', 'qweb')]) # # Trigger COW then break the QWEB XML on it @@ -80,7 +80,7 @@ class TestWebsiteResetViews(odoo.tests.HttpCase): # self.fix_it('/test_view') # also mute ir.ui.view as `get_view_id()` will raise "Could not find view object with xml_id 'not.exist'"" - @mute_logger('odoo.addons.website.models.ir_http', 'odoo.addons.website.models.ir_ui_view') + @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.addons.website.models.ir_ui_view') def test_06_reset_specific_view_controller_inexisting_template(self): total_views = self.View.search_count([('type', '=', 'qweb')]) # Trigger COW then break the QWEB XML on it @@ -88,14 +88,14 @@ class TestWebsiteResetViews(odoo.tests.HttpCase): self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (2)") self.fix_it('/test_view') - @mute_logger('odoo.addons.website.models.ir_http') + @mute_logger('odoo.addons.http_routing.models.ir_http') def test_07_reset_page_view_complete_flow(self): self.start_tour("/", 'test_reset_page_view_complete_flow_part1', login="admin") self.fix_it('/test_page_view') self.start_tour("/", 'test_reset_page_view_complete_flow_part2', login="admin") self.fix_it('/test_page_view') - @mute_logger('odoo.addons.website.models.ir_http') + @mute_logger('odoo.addons.http_routing.models.ir_http') def test_08_reset_specific_page_view_hard_mode(self): self.test_page_view = self.Website.viewref('test_website.test_page_view') total_views = self.View.search_count([('type', '=', 'qweb')]) diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 6d421357c90..ef9a9433ce7 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging from lxml import etree -import traceback import os import unittest @@ -14,14 +13,12 @@ import werkzeug.utils from functools import partial import odoo -from odoo import api, models, registry +from odoo import api, models from odoo import SUPERUSER_ID from odoo.http import request -from odoo.tools import config from odoo.tools.safe_eval import safe_eval from odoo.osv.expression import FALSE_DOMAIN, OR -from odoo.addons.base.models.qweb import QWebException from odoo.addons.http_routing.models.ir_http import ModelConverter, _guess_mimetype from odoo.addons.portal.controllers.portal import _build_url_w_params @@ -239,7 +236,6 @@ class Http(models.AbstractModel): 'deletable': True, 'main_object': mypage, }, mimetype=_guess_mimetype(ext)) - return False @classmethod def _serve_redirect(cls): @@ -269,109 +265,47 @@ class Http(models.AbstractModel): return False @classmethod - def _handle_exception(cls, exception): - code = 500 # default code - is_website_request = bool(getattr(request, 'is_frontend', False) and get_request_website()) - if not is_website_request: - # Don't touch non website requests exception handling - return super(Http, cls)._handle_exception(exception) - else: + def _get_exception_code_values(cls, exception): + code, values = super(Http, cls)._get_exception_code_values(exception) + if request.website.is_publisher() and isinstance(exception, werkzeug.exceptions.NotFound): + code = 'page_404' + values['path'] = request.httprequest.path[1:] + return (code, values) + + @classmethod + def _get_values_500_error(cls, env, values, exception): + View = env["ir.ui.view"] + values = super(Http, cls)._get_values_500_error(env, values, exception) + if 'qweb_exception' in values: try: - response = super(Http, cls)._handle_exception(exception) + # exception.name might be int, string + exception_template = int(exception.name) + except: + exception_template = exception.name + view = View._view_obj(exception_template) + if exception.html and exception.html in view.arch: + values['view'] = view + else: + # There might be 2 cases where the exception code can't be found + # in the view, either the error is in a child view or the code + # contains branding (
). + et = etree.fromstring(view.with_context(inherit_branding=False).read_combined(['arch'])['arch']) + node = et.xpath(exception.path) + line = node is not None and etree.tostring(node[0], encoding='unicode') + if line: + values['view'] = View._views_get(exception_template).filtered( + lambda v: line in v.arch + ) + values['view'] = values['view'] and values['view'][0] + # Needed to show reset template on translated pages (`_prepare_qcontext` will set it for main lang) + values['editable'] = request.uid and request.website.is_publisher() + return values - if isinstance(response, Exception): - exception = response - else: - # if parent excplicitely returns a plain response, then we don't touch it - return response - except Exception as e: - if 'werkzeug' in config['dev_mode']: - raise e - exception = e - - values = dict( - exception=exception, - traceback=traceback.format_exc(), - ) - - # only except_orm exceptions contain a message - if isinstance(exception, odoo.exceptions.except_orm): - values['error_message'] = exception.name - code = 400 - - 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, odoo.exceptions.AccessError): - code = 403 - - if isinstance(exception, QWebException): - values.update(qweb_exception=exception) - - # retro compatibility to remove in 12.2 - exception.qweb = dict(message=exception.message, expression=exception.html) - - if type(exception.error) == odoo.exceptions.AccessError: - code = 403 - - values.update( - status_message=werkzeug.http.HTTP_STATUS_CODES[code], - status_code=code, - ) - - view_id = code - if request.website.is_publisher() and isinstance(exception, werkzeug.exceptions.NotFound): - view_id = 'page_404' - values['path'] = request.httprequest.path[1:] - - if not request.uid: - cls._auth_method_public() - - with registry(request.env.cr.dbname).cursor() as cr: - env = api.Environment(cr, request.uid, request.env.context) - if code == 500: - logger.error("500 Internal Server Error:\n\n%s", values['traceback']) - View = env["ir.ui.view"] - values['view'] = View - if 'qweb_exception' in values: - try: - # exception.name might be int, string - exception_template = int(exception.name) - except: - exception_template = exception.name - view = View._view_obj(exception_template) - if exception.html and exception.html in view.arch: - values['view'] = view - else: - # There might be 2 cases where the exception code can't be found - # in the view, either the error is in a child view or the code - # contains branding (
). - et = etree.fromstring(view.with_context(inherit_branding=False).read_combined(['arch'])['arch']) - node = et.xpath(exception.path) - line = node is not None and etree.tostring(node[0], encoding='unicode') - if line: - values['view'] = View._views_get(exception_template).filtered( - lambda v: line in v.arch - ) - values['view'] = values['view'] and values['view'][0] - - # Needed to show reset template on translated pages (`_prepare_qcontext` will set it for main lang) - values['editable'] = request.uid and request.website.is_publisher() - elif code == 403: - logger.warn("403 Forbidden:\n\n%s", values['traceback']) - elif code == 400: - logger.warn("400 Bad Request:\n\n%s", values['traceback']) - try: - html = env['ir.ui.view'].render_template('website.%s' % view_id, values) - except Exception: - html = env['ir.ui.view'].render_template('website.http_error', values) - - return werkzeug.wrappers.Response(html, status=code, content_type='text/html;charset=utf-8') + @classmethod + def _get_error_html(cls, env, code, values): + if code == 'page_404': + return env['ir.ui.view'].render_template('website.%s' % code, values) + return super(Http, cls)._get_error_html(env, code, values) def binary_content(self, xmlid=None, model='ir.attachment', id=None, field='datas', unique=False, filename=None, filename_field='name', download=False, diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index d7a02f98437..30f3c8206ac 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -788,7 +788,7 @@ - - - - - - - - - - - - -