From 1e57ad207677386be4672ab5546c62544afe7ac6 Mon Sep 17 00:00:00 2001 From: Julien Castiaux Date: Tue, 6 Sep 2022 17:20:02 +0000 Subject: [PATCH] [FIX] base: no request.is_frontend attribute in RPC Install website then render a qweb template via xmlrpc, attribute error: `request` has no `is_frontend` attribute. Inside the qweb's `_prepare_environment` override of the http_routing module (a dependency of website) is the following code snippet: if (not irQweb.env.context.get('minimal_qcontext') and request and request.is_frontend): return irQweb._prepare_frontend_environment(values) The conditionnal is about injecting extra informations in the qweb's context in case we are serving a frontend request. By default, there is no `is_frontend` attribute on the request object. That attribute is set by the ir.http's `_match` override of the http_routing module (a website dependency): it is set True when we don't match any endpoint or that we match an endpoint that is `website=True`, it is set False otherwise. The ir.http's `_match` method is called whenever we are serving a http request whoose session is bound to a specific database. i.e. when there is a valid database saved in the request's session. When there is not database in the request's session (or that it is invalid) the matched endpoint is directly called without going throught ir.http. Most endpoints are only accessible via ir.http. The two `/xmlrpc` and `/jsonrpc` endpoints are examples of endpoint that do not require an established database connection to work. They perform the request authentication and database connection themselves. It is possible to call those two endpoints with no database saved in the session, thus it is possible to call those two endpoints without going throught ir.http. This is expected. The two endpoints's duty is to execute public model methods and return the xml/json serialized result. To do so, a registry is loaded on the database with all the installed modules, including http_routing. We fall in a situation where (1) there is a request, (2) we are using a registry where http_routing is loaded, (3) there is no `is_frontend` attribute on `request` as we didn't serve the endpoint via ir.http. This situation is illegale. To solve the problem, instead of working on the `if request.is_frontend` bit of the above conditional, we decided to work on the `if request` bit. ISO-model wise, RPC is an extra 8th layer built on top of HTTP. HTTP is merely a transparent transport between a RPC client and a RPC server, any other request-response capable procotol could fit. The method executed via RPC must be independant from the usage of HTTP as mean of transportation thus it should not be capable of using the current request. The proposed change is to temporary un-expose the current request from the local-stack during the execution of the RPC method. This fixes the problem as the code now run like it was executed from the shell or from a cron. The other benefit is that the pattern used inside the condition: `if request and request.is_frontend` doesn't need to change. X-original-commit: f28863bfdaac3f644fcb274c856e0770783ed75c Part-of: odoo/odoo#104567 --- odoo/addons/base/controllers/rpc.py | 12 +++++++----- odoo/addons/test_http/models.py | 8 +++++++- odoo/addons/test_http/tests/test_misc.py | 19 ++++++++++++++++++- odoo/http.py | 9 +++++++++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/odoo/addons/base/controllers/rpc.py b/odoo/addons/base/controllers/rpc.py index e883d1c0fd2..1b608130f3f 100644 --- a/odoo/addons/base/controllers/rpc.py +++ b/odoo/addons/base/controllers/rpc.py @@ -8,7 +8,7 @@ from markupsafe import Markup from werkzeug.wrappers import Response import odoo -from odoo.http import Controller, request, route +from odoo.http import Controller, borrow_request, route from odoo.fields import Date, Datetime, Command from odoo.service import dispatch_rpc from odoo.tools import lazy, ustr @@ -125,9 +125,10 @@ class RPC(Controller): def _xmlrpc(self, service): """Common method to handle an XML-RPC request.""" - data = request.httprequest.get_data() - params, method = xmlrpc.client.loads(data) - result = dispatch_rpc(service, method, params) + with borrow_request() as request: + data = request.httprequest.get_data() + params, method = xmlrpc.client.loads(data) + result = dispatch_rpc(service, method, params) return xmlrpc.client.dumps((result,), methodresponse=1, allow_none=False) @route("/xmlrpc/", auth="none", methods=["POST"], csrf=False, save_session=False) @@ -155,4 +156,5 @@ class RPC(Controller): @route('/jsonrpc', type='json', auth="none", save_session=False) def jsonrpc(self, service, method, args): """ Method used by client APIs to contact OpenERP. """ - return dispatch_rpc(service, method, args) + with borrow_request(): + return dispatch_rpc(service, method, args) diff --git a/odoo/addons/test_http/models.py b/odoo/addons/test_http/models.py index 21f548ccff2..c3bcef1aafb 100644 --- a/odoo/addons/test_http/models.py +++ b/odoo/addons/test_http/models.py @@ -56,4 +56,10 @@ class Galaxy(models.Model): _name = 'test_http.galaxy' _description = 'Galaxy' - name = fields.Char(required=True) + name = fields.Char(required=True, help='The galaxy common name.') + + @api.model + def render(self, galaxy_id): + return self.env['ir.qweb']._render('test_http.tmpl_galaxy', { + 'galaxy': self.browse([galaxy_id]) + }) diff --git a/odoo/addons/test_http/tests/test_misc.py b/odoo/addons/test_http/tests/test_misc.py index 855968d5f0a..749983e1436 100644 --- a/odoo/addons/test_http/tests/test_misc.py +++ b/odoo/addons/test_http/tests/test_misc.py @@ -8,7 +8,7 @@ from urllib.parse import urlparse import odoo from odoo.http import root from odoo.tests import tagged -from odoo.tests.common import HOST +from odoo.tests.common import HOST, new_test_user, get_db_name from odoo.tools import config, file_path from odoo.addons.test_http.controllers import CT_JSON @@ -75,6 +75,23 @@ class TestHttpMisc(TestHttpBase): self.assertIsNone(root.get_static_file(f'odoo.com/{uri}'), "No host allowed") self.assertIsNone(root.get_static_file(f'http://odoo.com/{uri}'), "No host allowed") + def test_misc4_rpc_qweb(self): + jack = new_test_user(self.env, 'jackoneill', context={'lang': 'en_US'}) + milky_way = self.env.ref('test_http.milky_way') + + payload = json.dumps({'jsonrpc': '2.0', 'method': 'call', 'id': None, 'params': { + 'service': 'object', 'method': 'execute', 'args': [ + get_db_name(), jack.id, 'jackoneill', 'test_http.galaxy', 'render', milky_way.id + ] + }}) + + res = self.nodb_url_open('/jsonrpc', data=payload, headers=CT_JSON) + res.raise_for_status() + + res_rpc = res.json() + self.assertNotIn('error', res_rpc.keys(), res_rpc.get('error', {}).get('data', {}).get('message')) + self.assertIn(milky_way.name, res_rpc['result'], "QWeb template was correctly rendered") + @tagged('post_install', '-at_install') class TestHttpCors(TestHttpBase): diff --git a/odoo/http.py b/odoo/http.py index e34dbf4783a..a7f5b19847a 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -975,6 +975,15 @@ class Session(collections.abc.MutableMapping): _request_stack = werkzeug.local.LocalStack() request = _request_stack() +@contextlib.contextmanager +def borrow_request(): + """ Get the current request and unexpose it from the local stack. """ + req = _request_stack.pop() + try: + yield req + finally: + _request_stack.push(req) + class Response(werkzeug.wrappers.Response): """