# Copyright 2018 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import logging from werkzeug import Response from werkzeug.exceptions import NotFound from odoo.http import request from odoo.addons.component.core import AbstractComponent from ..apispec.base_rest_service_apispec import BaseRestServiceAPISpec from ..tools import ROUTING_DECORATOR_ATTR _logger = logging.getLogger(__name__) def to_int(val): # The javascript VM ducktape only use float and so pass float # to the api, the werkzeug request interpret params as unicode # so we have to convert params from string to float to int if isinstance(val, int): return val if val: return int(float(val)) return None def to_bool(val): return val in ("true", "True", "1", True) def skip_secure_params(func): """ Used to decorate methods :param func: :return: """ func.skip_secure_params = True return func def skip_secure_response(func): """ Used to decorate methods :param func: :return: """ func.skip_secure_response = True return func class BaseRestService(AbstractComponent): _name = "base.rest.service" _description = None # description included into the openapi doc _is_rest_service_component = True # marker to retrieve REST components def _prepare_extra_log(self, func, params, secure_params, res): httprequest = request.httprequest headers = dict(httprequest.headers) return { "application": "Rest Service", "request_url": httprequest.url, "request_method": httprequest.method, "params": params, "headers": headers, "secure_params": secure_params, "res": res, "status": 200, } def _log_call(self, func, params, secure_params, res): """If you want to enjoy the advanced log install the module logging_json""" if request: httprequest = request.httprequest extra = self._prepare_extra_log(func, params, secure_params, res) args = [httprequest.url, httprequest.method] message = "REST call url %s method %s" _logger.debug(message, *args, extra=extra) def _prepare_input_params(self, method, params): """ Internal method used to process the input_param parameter. The result will be used to call the final method. The processing is delegated to the `resapi.RestMethodParam` instance specified by the restapi.method` decorator on the method. :param method: :param params: :return: """ method_name = method.__name__ if hasattr(method, "skip_secure_params"): return params routing = getattr(method, ROUTING_DECORATOR_ATTR, None) if not routing: _logger.warning( "Method %s is not a public method of service %s", method_name, self._name, ) raise NotFound() input_param = routing["input_param"] if input_param: return input_param.from_params(self, params) return {} def _prepare_response(self, method, result): """ Internal method used to process the result of the method called by the controller. The result of this process is returned to the controller The processing is delegated to the `resapi.RestMethodParam` instance specified by the `restapi.method` decorator on the method. :param method: method :param response: :return: dict/json or `http.Response` """ method_name = method if callable(method): method_name = method.__name__ if hasattr(method, "skip_secure_response"): return result routing = getattr(method, ROUTING_DECORATOR_ATTR, None) output_param = routing["output_param"] if not output_param: _logger.warning( "DEPRECATED: You must define an output schema for method %s " "in service %s", method_name, self._name, ) return result return output_param.to_response(self, result) def dispatch(self, method_name, *args, params=None): """ This method dispatch the call to the final method. Before the call parameters are processed by the `restapi.RestMethodParam` object specified as input_param object. The result of the method is therefore given to the `restapi.RestMethodParam` object specified as output_param to build the final response returned by the service :param method_name: :param *args: query path paramters args :param params: A dictionary with the parameters of the method. Once secured and sanitized, these parameters will be passed to the method as keyword args. :return: """ method = getattr(self, method_name, object()) params = params or {} secure_params = self._prepare_input_params(method, params) if isinstance(secure_params, dict): # for backward compatibility methods expecting json params # are declared as m(self, p1=None, p2=None) or m(self, **params) res = method(*args, **secure_params) else: res = method(*args, secure_params) self._log_call(method, params, secure_params, res) if isinstance(res, Response): return res return self._prepare_response(method, res) def _validator_delete(self): """ Default validator for delete method. By default delete should never be called with parameters. """ return {} def _validator_get(self): """ Default validator for get method. By default get should not be called with parameters. """ return {} def _get_api_spec(self, **params): return BaseRestServiceAPISpec(self, **params) def to_openapi(self, **params): """ Return the description of this REST service as an OpenAPI json document :return: json document """ api_spec = self._get_api_spec(**params) api_spec.generate_paths() return api_spec.to_dict() def _get_openapi_default_parameters(self): return [] def _get_openapi_default_responses(self): return { "400": {"description": "One of the given parameter is not valid"}, "401": { "description": "The user is not authorized. Authentication " "is required" }, "404": {"description": "Requested resource not found"}, "403": { "description": "You don't have the permission to access the " "requested resource." }, } @property def request(self): return self.work.request @property def controller(self): return self.work.controller