http.py 76 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. r"""\
  3. Odoo HTTP layer / WSGI application
  4. The main duty of this module is to prepare and dispatch all http
  5. requests to their corresponding controllers: from a raw http request
  6. arriving on the WSGI entrypoint to a :class:`~http.Request`: arriving at
  7. a module controller with a fully setup ORM available.
  8. Application developers mostly know this module thanks to the
  9. :class:`~odoo.http.Controller`: class and its companion the
  10. :func:`~odoo.http.route`: method decorator. Together they are used to
  11. register methods responsible of delivering web content to matching URLS.
  12. Those two are only the tip of the iceberg, below is an ascii graph that
  13. shows the various processing layers each request passes through before
  14. ending at the @route decorated endpoint. Hopefully, this graph and the
  15. attached function descriptions will help you understand this module.
  16. Here be dragons:
  17. Application.__call__
  18. +-> Request._serve_static
  19. |
  20. +-> Request._serve_nodb
  21. | -> App.nodb_routing_map.match
  22. | -> Dispatcher.pre_dispatch
  23. | -> Dispatcher.dispatch
  24. | -> route_wrapper
  25. | -> endpoint
  26. | -> Dispatcher.post_dispatch
  27. |
  28. +-> Request._serve_db
  29. -> model.retrying
  30. -> Request._serve_ir_http
  31. -> env['ir.http']._match
  32. -> env['ir.http']._authenticate
  33. -> env['ir.http']._pre_dispatch
  34. -> Dispatcher.pre_dispatch
  35. -> Dispatcher.dispatch
  36. -> env['ir.http']._dispatch
  37. -> route_wrapper
  38. -> endpoint
  39. -> env['ir.http']._post_dispatch
  40. -> Dispatcher.post_dispatch
  41. Application.__call__
  42. WSGI entry point, it sanitizes the request, it wraps it in a werkzeug
  43. request and itself in an Odoo http request. The Odoo http request is
  44. exposed at ``http.request`` then it is forwarded to either
  45. ``_serve_static``, ``_serve_nodb`` or ``_serve_db`` depending on the
  46. request path and the presence of a database. It is also responsible of
  47. ensuring any error is properly logged and encapsuled in a HTTP error
  48. response.
  49. Request._serve_static
  50. Handle all requests to ``/<module>/static/<asset>`` paths, open the
  51. underlying file on the filesystem and stream it via
  52. :meth:``Request.send_file``
  53. Request._serve_nodb
  54. Handle requests to ``@route(auth='none')`` endpoints when the user is
  55. not connected to a database. It performs limited operations, just
  56. matching the auth='none' endpoint using the request path and then it
  57. delegates to Dispatcher.
  58. Request._serve_db
  59. Handle all requests that are not static when it is possible to connect
  60. to a database. It opens a session and initializes the ORM before
  61. forwarding the request to ``retrying`` and ``_serve_ir_http``.
  62. service.model.retrying
  63. Protect against SQL serialisation errors (when two different
  64. transactions write on the same record), when such an error occurs this
  65. function resets the session and the environment then re-dispatches the
  66. request.
  67. Request._serve_ir_http
  68. Delegate most of the effort to the ``ir.http`` abstract model which
  69. itself calls RequestDispatch back. ``ir.http`` grants modularity in
  70. the http stack. The notable difference with nodb is that there is an
  71. authentication layer and a mechanism to serve pages that are not
  72. accessible through controllers.
  73. ir.http._authenticate
  74. Ensure the user on the current environment fulfill the requirement of
  75. ``@route(auth=...)``. Using the ORM outside of abstract models is
  76. unsafe prior of calling this function.
  77. ir.http._pre_dispatch/Dispatcher.pre_dispatch
  78. Prepare the system the handle the current request, often used to save
  79. some extra query-string parameters in the session (e.g. ?debug=1)
  80. ir.http._dispatch/Dispatcher.dispatch
  81. Deserialize the HTTP request body into ``request.params`` according to
  82. @route(type=...), call the controller endpoint, serialize its return
  83. value into an HTTP Response object.
  84. ir.http._post_dispatch/Dispatcher.post_dispatch
  85. Post process the response returned by the controller endpoint. Used to
  86. inject various headers such as Content-Security-Policy.
  87. route_wrapper, closure of the http.route decorator
  88. Sanitize the request parameters, call the route endpoint and
  89. optionally coerce the endpoint result.
  90. endpoint
  91. The @route(...) decorated controller method.
  92. """
  93. import base64
  94. import cgi
  95. import collections
  96. import collections.abc
  97. import contextlib
  98. import functools
  99. import glob
  100. import hashlib
  101. import hmac
  102. import inspect
  103. import json
  104. import logging
  105. import mimetypes
  106. import os
  107. import re
  108. import threading
  109. import time
  110. import traceback
  111. import warnings
  112. import zlib
  113. from abc import ABC, abstractmethod
  114. from datetime import datetime
  115. from io import BytesIO
  116. from os.path import join as opj
  117. from pathlib import Path
  118. from urllib.parse import urlparse
  119. from zlib import adler32
  120. import babel.core
  121. import psycopg2
  122. import werkzeug.datastructures
  123. import werkzeug.exceptions
  124. import werkzeug.local
  125. import werkzeug.routing
  126. import werkzeug.security
  127. import werkzeug.wrappers
  128. import werkzeug.wsgi
  129. from werkzeug.urls import URL, url_parse, url_encode, url_quote
  130. from werkzeug.exceptions import (HTTPException, BadRequest, Forbidden,
  131. NotFound, InternalServerError)
  132. try:
  133. from werkzeug.middleware.proxy_fix import ProxyFix as ProxyFix_
  134. ProxyFix = functools.partial(ProxyFix_, x_for=1, x_proto=1, x_host=1)
  135. except ImportError:
  136. from werkzeug.contrib.fixers import ProxyFix
  137. try:
  138. from werkzeug.utils import send_file as _send_file
  139. except ImportError:
  140. from .tools._vendor.send_file import send_file as _send_file
  141. import odoo
  142. from .exceptions import UserError, AccessError, AccessDenied
  143. from .modules.module import get_manifest
  144. from .modules.registry import Registry
  145. from .service import security, model as service_model
  146. from .tools import (config, consteq, date_utils, file_path, parse_version,
  147. profiler, submap, unique, ustr,)
  148. from .tools.geoipresolver import GeoIPResolver
  149. from .tools.func import filter_kwargs, lazy_property
  150. from .tools.mimetypes import guess_mimetype
  151. from .tools.misc import pickle
  152. from .tools._vendor import sessions
  153. from .tools._vendor.useragents import UserAgent
  154. _logger = logging.getLogger(__name__)
  155. # =========================================================
  156. # Lib fixes
  157. # =========================================================
  158. # Add potentially missing (older ubuntu) font mime types
  159. mimetypes.add_type('application/font-woff', '.woff')
  160. mimetypes.add_type('application/vnd.ms-fontobject', '.eot')
  161. mimetypes.add_type('application/x-font-ttf', '.ttf')
  162. # Add potentially wrong (detected on windows) svg mime types
  163. mimetypes.add_type('image/svg+xml', '.svg')
  164. # To remove when corrected in Babel
  165. babel.core.LOCALE_ALIASES['nb'] = 'nb_NO'
  166. # =========================================================
  167. # Const
  168. # =========================================================
  169. # The validity duration of a preflight response, one day.
  170. CORS_MAX_AGE = 60 * 60 * 24
  171. # The HTTP methods that do not require a CSRF validation.
  172. CSRF_FREE_METHODS = ('GET', 'HEAD', 'OPTIONS', 'TRACE')
  173. # The default csrf token lifetime, a salt against BREACH, one year
  174. CSRF_TOKEN_SALT = 60 * 60 * 24 * 365
  175. # The default lang to use when the browser doesn't specify it
  176. DEFAULT_LANG = 'en_US'
  177. # The dictionary to initialise a new session with.
  178. def get_default_session():
  179. return {
  180. 'context': {}, # 'lang': request.default_lang() # must be set at runtime
  181. 'db': None,
  182. 'debug': '',
  183. 'login': None,
  184. 'uid': None,
  185. 'session_token': None,
  186. }
  187. # The request mimetypes that transport JSON in their body.
  188. JSON_MIMETYPES = ('application/json', 'application/json-rpc')
  189. MISSING_CSRF_WARNING = """\
  190. No CSRF validation token provided for path %r
  191. Odoo URLs are CSRF-protected by default (when accessed with unsafe
  192. HTTP methods). See
  193. https://www.odoo.com/documentation/16.0/developer/reference/addons/http.html#csrf
  194. for more details.
  195. * if this endpoint is accessed through Odoo via py-QWeb form, embed a CSRF
  196. token in the form, Tokens are available via `request.csrf_token()`
  197. can be provided through a hidden input and must be POST-ed named
  198. `csrf_token` e.g. in your form add:
  199. <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
  200. * if the form is generated or posted in javascript, the token value is
  201. available as `csrf_token` on `web.core` and as the `csrf_token`
  202. value in the default js-qweb execution context
  203. * if the form is accessed by an external third party (e.g. REST API
  204. endpoint, payment gateway callback) you will need to disable CSRF
  205. protection (and implement your own protection if necessary) by
  206. passing the `csrf=False` parameter to the `route` decorator.
  207. """
  208. # The @route arguments to propagate from the decorated method to the
  209. # routing rule.
  210. ROUTING_KEYS = {
  211. 'defaults', 'subdomain', 'build_only', 'strict_slashes', 'redirect_to',
  212. 'alias', 'host', 'methods',
  213. }
  214. if parse_version(werkzeug.__version__) >= parse_version('2.0.2'):
  215. # Werkzeug 2.0.2 adds the websocket option. If a websocket request
  216. # (ws/wss) is trying to access an HTTP route, a WebsocketMismatch
  217. # exception is raised. On the other hand, Werkzeug 0.16 does not
  218. # support the websocket routing key. In order to bypass this issue,
  219. # let's add the websocket key only when appropriate.
  220. ROUTING_KEYS.add('websocket')
  221. # The default duration of a user session cookie. Inactive sessions are reaped
  222. # server-side as well with a threshold that can be set via an optional
  223. # config parameter `sessions.max_inactivity_seconds` (default: SESSION_LIFETIME)
  224. SESSION_LIFETIME = 60 * 60 * 24 * 7
  225. # The cache duration for static content from the filesystem, one week.
  226. STATIC_CACHE = 60 * 60 * 24 * 7
  227. # The cache duration for content where the url uniquely identifies the
  228. # content (usually using a hash), one year.
  229. STATIC_CACHE_LONG = 60 * 60 * 24 * 365
  230. # =========================================================
  231. # Helpers
  232. # =========================================================
  233. class SessionExpiredException(Exception):
  234. pass
  235. def content_disposition(filename):
  236. return "attachment; filename*=UTF-8''{}".format(
  237. url_quote(filename, safe='')
  238. )
  239. def db_list(force=False, host=None):
  240. """
  241. Get the list of available databases.
  242. :param bool force: See :func:`~odoo.service.db.list_dbs`:
  243. :param host: The Host used to replace %h and %d in the dbfilters
  244. regexp. Taken from the current request when omitted.
  245. :returns: the list of available databases
  246. :rtype: List[str]
  247. """
  248. try:
  249. dbs = odoo.service.db.list_dbs(force)
  250. except psycopg2.OperationalError:
  251. return []
  252. return db_filter(dbs, host)
  253. def db_filter(dbs, host=None):
  254. """
  255. Return the subset of ``dbs`` that match the dbfilter or the dbname
  256. server configuration. In case neither are configured, return ``dbs``
  257. as-is.
  258. :param Iterable[str] dbs: The list of database names to filter.
  259. :param host: The Host used to replace %h and %d in the dbfilters
  260. regexp. Taken from the current request when omitted.
  261. :returns: The original list filtered.
  262. :rtype: List[str]
  263. """
  264. if config['dbfilter']:
  265. # host
  266. # -----------
  267. # www.example.com:80
  268. # -------
  269. # domain
  270. if host is None:
  271. host = request.httprequest.environ.get('HTTP_HOST', '')
  272. host = host.partition(':')[0]
  273. if host.startswith('www.'):
  274. host = host[4:]
  275. domain = host.partition('.')[0]
  276. dbfilter_re = re.compile(
  277. config["dbfilter"].replace("%h", re.escape(host))
  278. .replace("%d", re.escape(domain)))
  279. return [db for db in dbs if dbfilter_re.match(db)]
  280. if config['db_name']:
  281. # In case --db-filter is not provided and --database is passed, Odoo will
  282. # use the value of --database as a comma separated list of exposed databases.
  283. exposed_dbs = {db.strip() for db in config['db_name'].split(',')}
  284. return sorted(exposed_dbs.intersection(dbs))
  285. return list(dbs)
  286. def dispatch_rpc(service_name, method, params):
  287. """
  288. Perform a RPC call.
  289. :param str service_name: either "common", "db" or "object".
  290. :param str method: the method name of the given service to execute
  291. :param Mapping params: the keyword arguments for method call
  292. :return: the return value of the called method
  293. :rtype: Any
  294. """
  295. rpc_dispatchers = {
  296. 'common': odoo.service.common.dispatch,
  297. 'db': odoo.service.db.dispatch,
  298. 'object': odoo.service.model.dispatch,
  299. }
  300. with borrow_request():
  301. threading.current_thread().uid = None
  302. threading.current_thread().dbname = None
  303. dispatch = rpc_dispatchers[service_name]
  304. return dispatch(method, params)
  305. def is_cors_preflight(request, endpoint):
  306. return request.httprequest.method == 'OPTIONS' and endpoint.routing.get('cors', False)
  307. def serialize_exception(exception):
  308. name = type(exception).__name__
  309. module = type(exception).__module__
  310. return {
  311. 'name': f'{module}.{name}' if module else name,
  312. 'debug': traceback.format_exc(),
  313. 'message': ustr(exception),
  314. 'arguments': exception.args,
  315. 'context': getattr(exception, 'context', {}),
  316. }
  317. # =========================================================
  318. # File Streaming
  319. # =========================================================
  320. def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None, mtime=None,
  321. add_etags=True, cache_timeout=STATIC_CACHE, conditional=True):
  322. warnings.warn('odoo.http.send_file is deprecated, please use odoo.http.Stream instead.', DeprecationWarning, stacklevel=2)
  323. return _send_file(
  324. filepath_or_fp,
  325. request.httprequest.environ,
  326. mimetype=mimetype,
  327. as_attachment=as_attachment,
  328. download_name=filename,
  329. last_modified=mtime,
  330. etag=add_etags,
  331. max_age=cache_timeout,
  332. response_class=Response,
  333. conditional=conditional
  334. )
  335. class Stream:
  336. """
  337. Send the content of a file, an attachment or a binary field via HTTP
  338. This utility is safe, cache-aware and uses the best available
  339. streaming strategy. Works best with the --x-sendfile cli option.
  340. Create a Stream via one of the constructors: :meth:`~from_path`:,
  341. :meth:`~from_attachment`: or :meth:`~from_binary_field`:, generate
  342. the corresponding HTTP response object via :meth:`~get_response`:.
  343. Instantiating a Stream object manually without using one of the
  344. dedicated constructors is discouraged.
  345. """
  346. type: str = '' # 'data' or 'path' or 'url'
  347. data = None
  348. path = None
  349. url = None
  350. mimetype = None
  351. as_attachment = False
  352. download_name = None
  353. conditional = True
  354. etag = True
  355. last_modified = None
  356. max_age = None
  357. immutable = False
  358. size = None
  359. def __init__(self, **kwargs):
  360. self.__dict__.update(kwargs)
  361. @classmethod
  362. def from_path(cls, path, filter_ext=('',)):
  363. """ Create a :class:`~Stream`: from an addon resource. """
  364. path = file_path(path, filter_ext)
  365. check = adler32(path.encode())
  366. stat = os.stat(path)
  367. return cls(
  368. type='path',
  369. path=path,
  370. download_name=os.path.basename(path),
  371. etag=f'{int(stat.st_mtime)}-{stat.st_size}-{check}',
  372. last_modified=stat.st_mtime,
  373. size=stat.st_size,
  374. )
  375. @classmethod
  376. def from_attachment(cls, attachment):
  377. """ Create a :class:`~Stream`: from an ir.attachment record. """
  378. attachment.ensure_one()
  379. self = cls(
  380. mimetype=attachment.mimetype,
  381. download_name=attachment.name,
  382. conditional=True,
  383. etag=attachment.checksum,
  384. )
  385. if attachment.store_fname:
  386. self.type = 'path'
  387. self.path = werkzeug.security.safe_join(
  388. os.path.abspath(config.filestore(request.db)),
  389. attachment.store_fname
  390. )
  391. stat = os.stat(self.path)
  392. self.last_modified = stat.st_mtime
  393. self.size = stat.st_size
  394. elif attachment.db_datas:
  395. self.type = 'data'
  396. self.data = attachment.raw
  397. self.last_modified = attachment['__last_update']
  398. self.size = len(self.data)
  399. elif attachment.url:
  400. # When the URL targets a file located in an addon, assume it
  401. # is a path to the resource. It saves an indirection and
  402. # stream the file right away.
  403. static_path = root.get_static_file(
  404. attachment.url,
  405. host=request.httprequest.environ.get('HTTP_HOST', '')
  406. )
  407. if static_path:
  408. self = cls.from_path(static_path)
  409. else:
  410. self.type = 'url'
  411. self.url = attachment.url
  412. else:
  413. self.type = 'data'
  414. self.data = b''
  415. self.size = 0
  416. return self
  417. @classmethod
  418. def from_binary_field(cls, record, field_name):
  419. """ Create a :class:`~Stream`: from a binary field. """
  420. data_b64 = record[field_name]
  421. data = base64.b64decode(data_b64) if data_b64 else b''
  422. return cls(
  423. type='data',
  424. data=data,
  425. etag=request.env['ir.attachment']._compute_checksum(data),
  426. last_modified=record['__last_update'] if record._log_access else None,
  427. size=len(data),
  428. )
  429. def read(self):
  430. """ Get the stream content as bytes. """
  431. if self.type == 'url':
  432. raise ValueError("Cannot read an URL")
  433. if self.type == 'data':
  434. return self.data
  435. with open(self.path, 'rb') as file:
  436. return file.read()
  437. def get_response(self, as_attachment=None, immutable=None, **send_file_kwargs):
  438. """
  439. Create the corresponding :class:`~Response` for the current stream.
  440. :param bool as_attachment: Indicate to the browser that it
  441. should offer to save the file instead of displaying it.
  442. :param bool immutable: Add the ``immutable`` directive to the
  443. ``Cache-Control`` response header, allowing intermediary
  444. proxies to aggressively cache the response. This option
  445. also set the ``max-age`` directive to 1 year.
  446. :param send_file_kwargs: Other keyword arguments to send to
  447. :func:`odoo.tools._vendor.send_file.send_file` instead of
  448. the stream sensitive values. Discouraged.
  449. """
  450. assert self.type in ('url', 'data', 'path'), "Invalid type: {self.type!r}, should be 'url', 'data' or 'path'."
  451. assert getattr(self, self.type) is not None, "There is nothing to stream, missing {self.type!r} attribute."
  452. if self.type == 'url':
  453. return request.redirect(self.url, code=301, local=False)
  454. if as_attachment is None:
  455. as_attachment = self.as_attachment
  456. if immutable is None:
  457. immutable = self.immutable
  458. send_file_kwargs = {
  459. 'mimetype': self.mimetype,
  460. 'as_attachment': as_attachment,
  461. 'download_name': self.download_name,
  462. 'conditional': self.conditional,
  463. 'etag': self.etag,
  464. 'last_modified': self.last_modified,
  465. 'max_age': STATIC_CACHE_LONG if immutable else self.max_age,
  466. 'environ': request.httprequest.environ,
  467. 'response_class': Response,
  468. **send_file_kwargs,
  469. }
  470. if self.type == 'data':
  471. return _send_file(BytesIO(self.data), **send_file_kwargs)
  472. # self.type == 'path'
  473. send_file_kwargs['use_x_sendfile'] = False
  474. if config['x_sendfile']:
  475. with contextlib.suppress(ValueError): # outside of the filestore
  476. fspath = Path(self.path).relative_to(opj(config['data_dir'], 'filestore'))
  477. x_accel_redirect = f'/web/filestore/{fspath}'
  478. send_file_kwargs['use_x_sendfile'] = True
  479. res = _send_file(self.path, **send_file_kwargs)
  480. if immutable and res.cache_control:
  481. res.cache_control["immutable"] = None # None sets the directive
  482. if 'X-Sendfile' in res.headers:
  483. res.headers['X-Accel-Redirect'] = x_accel_redirect
  484. # In case of X-Sendfile/X-Accel-Redirect, the body is empty,
  485. # yet werkzeug gives the length of the file. This makes
  486. # NGINX wait for content that'll never arrive.
  487. res.headers['Content-Length'] = '0'
  488. return res
  489. # =========================================================
  490. # Controller and routes
  491. # =========================================================
  492. class Controller:
  493. """
  494. Class mixin that provide module controllers the ability to serve
  495. content over http and to be extended in child modules.
  496. Each class :ref:`inheriting <python:tut-inheritance>` from
  497. :class:`~odoo.http.Controller` can use the :func:`~odoo.http.route`:
  498. decorator to route matching incoming web requests to decorated
  499. methods.
  500. Like models, controllers can be extended by other modules. The
  501. extension mechanism is different because controllers can work in a
  502. database-free environment and therefore cannot use
  503. :class:~odoo.api.Registry:.
  504. To *override* a controller, :ref:`inherit <python:tut-inheritance>`
  505. from its class, override relevant methods and re-expose them with
  506. :func:`~odoo.http.route`:. Please note that the decorators of all
  507. methods are combined, if the overriding method’s decorator has no
  508. argument all previous ones will be kept, any provided argument will
  509. override previously defined ones.
  510. .. code-block:
  511. class GreetingController(odoo.http.Controller):
  512. @route('/greet', type='http', auth='public')
  513. def greeting(self):
  514. return 'Hello'
  515. class UserGreetingController(GreetingController):
  516. @route(auth='user') # override auth, keep path and type
  517. def greeting(self):
  518. return super().handler()
  519. """
  520. children_classes = collections.defaultdict(list) # indexed by module
  521. @classmethod
  522. def __init_subclass__(cls):
  523. super().__init_subclass__()
  524. if Controller in cls.__bases__:
  525. path = cls.__module__.split('.')
  526. module = path[2] if path[:2] == ['odoo', 'addons'] else ''
  527. Controller.children_classes[module].append(cls)
  528. def route(route=None, **routing):
  529. """
  530. Decorate a controller method in order to route incoming requests
  531. matching the given URL and options to the decorated method.
  532. .. warning::
  533. It is mandatory to re-decorate any method that is overridden in
  534. controller extensions but the arguments can be omitted. See
  535. :class:`~odoo.http.Controller` for more details.
  536. :param Union[str, Iterable[str]] route: The paths that the decorated
  537. method is serving. Incoming HTTP request paths matching this
  538. route will be routed to this decorated method. See `werkzeug
  539. routing documentation <http://werkzeug.pocoo.org/docs/routing/>`_
  540. for the format of route expressions.
  541. :param str type: The type of request, either ``'json'`` or
  542. ``'http'``. It describes where to find the request parameters
  543. and how to serialize the response.
  544. :param str auth: The authentication method, one of the following:
  545. * ``'user'``: The user must be authenticated and the current
  546. request will be executed using the rights of the user.
  547. * ``'public'``: The user may or may not be authenticated. If he
  548. isn't, the current request will be executed using the shared
  549. Public user.
  550. * ``'none'``: The method is always active, even if there is no
  551. database. Mainly used by the framework and authentication
  552. modules. The request code will not have any facilities to
  553. access the current user.
  554. :param Iterable[str] methods: A list of http methods (verbs) this
  555. route applies to. If not specified, all methods are allowed.
  556. :param str cors: The Access-Control-Allow-Origin cors directive value.
  557. :param bool csrf: Whether CSRF protection should be enabled for the
  558. route. Enabled by default for ``'http'``-type requests, disabled
  559. by default for ``'json'``-type requests.
  560. """
  561. def decorator(endpoint):
  562. fname = f"<function {endpoint.__module__}.{endpoint.__name__}>"
  563. # Sanitize the routing
  564. assert routing.get('type', 'http') in _dispatchers.keys()
  565. if route:
  566. routing['routes'] = route if isinstance(route, list) else [route]
  567. wrong = routing.pop('method', None)
  568. if wrong is not None:
  569. _logger.warning("%s defined with invalid routing parameter 'method', assuming 'methods'", fname)
  570. routing['methods'] = wrong
  571. @functools.wraps(endpoint)
  572. def route_wrapper(self, *args, **params):
  573. params_ok = filter_kwargs(endpoint, params)
  574. params_ko = set(params) - set(params_ok)
  575. if params_ko:
  576. _logger.warning("%s called ignoring args %s", fname, params_ko)
  577. result = endpoint(self, *args, **params_ok)
  578. if routing['type'] == 'http': # _generate_routing_rules() ensures type is set
  579. return Response.load(result)
  580. return result
  581. route_wrapper.original_routing = routing
  582. route_wrapper.original_endpoint = endpoint
  583. return route_wrapper
  584. return decorator
  585. def _generate_routing_rules(modules, nodb_only, converters=None):
  586. """
  587. Two-fold algorithm used to (1) determine which method in the
  588. controller inheritance tree should bind to what URL with respect to
  589. the list of installed modules and (2) merge the various @route
  590. arguments of said method with the @route arguments of the method it
  591. overrides.
  592. """
  593. def is_valid(cls):
  594. """ Determine if the class is defined in an addon. """
  595. path = cls.__module__.split('.')
  596. return path[:2] == ['odoo', 'addons'] and path[2] in modules
  597. def get_leaf_classes(cls):
  598. """
  599. Find the classes that have no child and that have ``cls`` as
  600. ancestor.
  601. """
  602. result = []
  603. for subcls in cls.__subclasses__():
  604. if is_valid(subcls):
  605. result.extend(get_leaf_classes(subcls))
  606. if not result and is_valid(cls):
  607. result.append(cls)
  608. return result
  609. def build_controllers():
  610. """
  611. Create dummy controllers that inherit only from the controllers
  612. defined at the given ``modules`` (often system wide modules or
  613. installed modules). Modules in this context are Odoo addons.
  614. """
  615. # Controllers defined outside of odoo addons are outside of the
  616. # controller inheritance/extension mechanism.
  617. yield from (ctrl() for ctrl in Controller.children_classes.get('', []))
  618. # Controllers defined inside of odoo addons can be extended in
  619. # other installed addons. Rebuild the class inheritance here.
  620. highest_controllers = []
  621. for module in modules:
  622. highest_controllers.extend(Controller.children_classes.get(module, []))
  623. for top_ctrl in highest_controllers:
  624. leaf_controllers = list(unique(get_leaf_classes(top_ctrl)))
  625. name = top_ctrl.__name__
  626. if leaf_controllers != [top_ctrl]:
  627. name += ' (extended by %s)' % ', '.join(
  628. bot_ctrl.__name__
  629. for bot_ctrl in leaf_controllers
  630. if bot_ctrl is not top_ctrl
  631. )
  632. Ctrl = type(name, tuple(reversed(leaf_controllers)), {})
  633. yield Ctrl()
  634. for ctrl in build_controllers():
  635. for method_name, method in inspect.getmembers(ctrl, inspect.ismethod):
  636. # Skip this method if it is not @route decorated anywhere in
  637. # the hierarchy
  638. def is_method_a_route(cls):
  639. return getattr(getattr(cls, method_name, None), 'original_routing', None) is not None
  640. if not any(map(is_method_a_route, type(ctrl).mro())):
  641. continue
  642. merged_routing = {
  643. # 'type': 'http', # set below
  644. 'auth': 'user',
  645. 'methods': None,
  646. 'routes': [],
  647. 'readonly': False,
  648. }
  649. for cls in unique(reversed(type(ctrl).mro()[:-2])): # ancestors first
  650. if method_name not in cls.__dict__:
  651. continue
  652. submethod = getattr(cls, method_name)
  653. if not hasattr(submethod, 'original_routing'):
  654. _logger.warning("The endpoint %s is not decorated by @route(), decorating it myself.", f'{cls.__module__}.{cls.__name__}.{method_name}')
  655. submethod = route()(submethod)
  656. # Ensure "type" is defined on each method's own routing,
  657. # also ensure overrides don't change the routing type.
  658. default_type = submethod.original_routing.get('type', 'http')
  659. routing_type = merged_routing.setdefault('type', default_type)
  660. if submethod.original_routing.get('type') not in (None, routing_type):
  661. _logger.warning("The endpoint %s changes the route type, using the original type: %r.", f'{cls.__module__}.{cls.__name__}.{method_name}', routing_type)
  662. submethod.original_routing['type'] = routing_type
  663. merged_routing.update(submethod.original_routing)
  664. if not merged_routing['routes']:
  665. _logger.warning("%s is a controller endpoint without any route, skipping.", f'{cls.__module__}.{cls.__name__}.{method_name}')
  666. continue
  667. if nodb_only and merged_routing['auth'] != "none":
  668. continue
  669. for url in merged_routing['routes']:
  670. # duplicates the function (partial) with a copy of the
  671. # original __dict__ (update_wrapper) to keep a reference
  672. # to `original_routing` and `original_endpoint`, assign
  673. # the merged routing ONLY on the duplicated function to
  674. # ensure method's immutability.
  675. endpoint = functools.partial(method)
  676. functools.update_wrapper(endpoint, method)
  677. endpoint.routing = merged_routing
  678. yield (url, endpoint)
  679. # =========================================================
  680. # Session
  681. # =========================================================
  682. class FilesystemSessionStore(sessions.FilesystemSessionStore):
  683. """ Place where to load and save session objects. """
  684. def get_session_filename(self, sid):
  685. # scatter sessions across 256 directories
  686. sha_dir = sid[:2]
  687. dirname = os.path.join(self.path, sha_dir)
  688. session_path = os.path.join(dirname, sid)
  689. return session_path
  690. def save(self, session):
  691. session_path = self.get_session_filename(session.sid)
  692. dirname = os.path.dirname(session_path)
  693. if not os.path.isdir(dirname):
  694. with contextlib.suppress(OSError):
  695. os.mkdir(dirname, 0o0755)
  696. super().save(session)
  697. def get(self, sid):
  698. # retro compatibility
  699. old_path = super().get_session_filename(sid)
  700. session_path = self.get_session_filename(sid)
  701. if os.path.isfile(old_path) and not os.path.isfile(session_path):
  702. dirname = os.path.dirname(session_path)
  703. if not os.path.isdir(dirname):
  704. with contextlib.suppress(OSError):
  705. os.mkdir(dirname, 0o0755)
  706. with contextlib.suppress(OSError):
  707. os.rename(old_path, session_path)
  708. return super().get(sid)
  709. def rotate(self, session, env):
  710. self.delete(session)
  711. session.sid = self.generate_key()
  712. if session.uid and env:
  713. session.session_token = security.compute_session_token(session, env)
  714. session.should_rotate = False
  715. self.save(session)
  716. def vacuum(self, max_lifetime=SESSION_LIFETIME):
  717. threshold = time.time() - max_lifetime
  718. for fname in glob.iglob(os.path.join(root.session_store.path, '*', '*')):
  719. path = os.path.join(root.session_store.path, fname)
  720. with contextlib.suppress(OSError):
  721. if os.path.getmtime(path) < threshold:
  722. os.unlink(path)
  723. class Session(collections.abc.MutableMapping):
  724. """ Structure containing data persisted across requests. """
  725. __slots__ = ('can_save', '_Session__data', 'is_dirty', 'is_explicit', 'is_new',
  726. 'should_rotate', 'sid')
  727. def __init__(self, data, sid, new=False):
  728. self.can_save = True
  729. self.__data = {}
  730. self.update(data)
  731. self.is_dirty = False
  732. self.is_explicit = False
  733. self.is_new = new
  734. self.should_rotate = False
  735. self.sid = sid
  736. #
  737. # MutableMapping implementation with DocDict-like extension
  738. #
  739. def __getitem__(self, item):
  740. if item == 'geoip':
  741. warnings.warn('request.session.geoip have been moved to request.geoip', DeprecationWarning)
  742. return request.geoip if request else {}
  743. return self.__data[item]
  744. def __setitem__(self, item, value):
  745. value = pickle.loads(pickle.dumps(value))
  746. if item not in self.__data or self.__data[item] != value:
  747. self.is_dirty = True
  748. self.__data[item] = value
  749. def __delitem__(self, item):
  750. del self.__data[item]
  751. self.is_dirty = True
  752. def __len__(self):
  753. return len(self.__data)
  754. def __iter__(self):
  755. return iter(self.__data)
  756. def __getattr__(self, attr):
  757. return self.get(attr, None)
  758. def __setattr__(self, key, val):
  759. if key in self.__slots__:
  760. super().__setattr__(key, val)
  761. else:
  762. self[key] = val
  763. def clear(self):
  764. self.__data.clear()
  765. self.is_dirty = True
  766. #
  767. # Session methods
  768. #
  769. def authenticate(self, dbname, login=None, password=None):
  770. """
  771. Authenticate the current user with the given db, login and
  772. password. If successful, store the authentication parameters in
  773. the current session, unless multi-factor-auth (MFA) is
  774. activated. In that case, that last part will be done by
  775. :ref:`finalize`.
  776. .. versionchanged:: saas-15.3
  777. The current request is no longer updated using the user and
  778. context of the session when the authentication is done using
  779. a database different than request.db. It is up to the caller
  780. to open a new cursor/registry/env on the given database.
  781. """
  782. wsgienv = {
  783. 'interactive': True,
  784. 'base_location': request.httprequest.url_root.rstrip('/'),
  785. 'HTTP_HOST': request.httprequest.environ['HTTP_HOST'],
  786. 'REMOTE_ADDR': request.httprequest.environ['REMOTE_ADDR'],
  787. }
  788. registry = Registry(dbname)
  789. pre_uid = registry['res.users'].authenticate(dbname, login, password, wsgienv)
  790. self.uid = None
  791. self.pre_login = login
  792. self.pre_uid = pre_uid
  793. with registry.cursor() as cr:
  794. env = odoo.api.Environment(cr, pre_uid, {})
  795. # if 2FA is disabled we finalize immediately
  796. user = env['res.users'].browse(pre_uid)
  797. if not user._mfa_url():
  798. self.finalize(env)
  799. if request and request.session is self and request.db == dbname:
  800. # Like update_env(user=request.session.uid) but works when uid is None
  801. request.env = odoo.api.Environment(request.env.cr, self.uid, self.context)
  802. request.update_context(**self.context)
  803. return pre_uid
  804. def finalize(self, env):
  805. """
  806. Finalizes a partial session, should be called on MFA validation
  807. to convert a partial / pre-session into a logged-in one.
  808. """
  809. login = self.pop('pre_login')
  810. uid = self.pop('pre_uid')
  811. env = env(user=uid)
  812. user_context = dict(env['res.users'].context_get())
  813. self.should_rotate = True
  814. self.update({
  815. 'db': env.registry.db_name,
  816. 'login': login,
  817. 'uid': uid,
  818. 'context': user_context,
  819. 'session_token': env.user._compute_session_token(self.sid),
  820. })
  821. def logout(self, keep_db=False):
  822. db = self.db if keep_db else get_default_session()['db'] # None
  823. debug = self.debug
  824. self.clear()
  825. self.update(get_default_session(), db=db, debug=debug)
  826. self.context['lang'] = request.default_lang() if request else DEFAULT_LANG
  827. self.should_rotate = True
  828. def touch(self):
  829. self.is_dirty = True
  830. # =========================================================
  831. # Request and Response
  832. # =========================================================
  833. # Thread local global request object
  834. _request_stack = werkzeug.local.LocalStack()
  835. request = _request_stack()
  836. @contextlib.contextmanager
  837. def borrow_request():
  838. """ Get the current request and unexpose it from the local stack. """
  839. req = _request_stack.pop()
  840. try:
  841. yield req
  842. finally:
  843. _request_stack.push(req)
  844. class Response(werkzeug.wrappers.Response):
  845. """
  846. Outgoing HTTP response with body, status, headers and qweb support.
  847. In addition to the :class:`werkzeug.wrappers.Response` parameters,
  848. this class's constructor can take the following additional
  849. parameters for QWeb Lazy Rendering.
  850. :param str template: template to render
  851. :param dict qcontext: Rendering context to use
  852. :param int uid: User id to use for the ir.ui.view render call,
  853. ``None`` to use the request's user (the default)
  854. these attributes are available as parameters on the Response object
  855. and can be altered at any time before rendering
  856. Also exposes all the attributes and methods of
  857. :class:`werkzeug.wrappers.Response`.
  858. """
  859. default_mimetype = 'text/html'
  860. def __init__(self, *args, **kw):
  861. template = kw.pop('template', None)
  862. qcontext = kw.pop('qcontext', None)
  863. uid = kw.pop('uid', None)
  864. super().__init__(*args, **kw)
  865. self.set_default(template, qcontext, uid)
  866. @classmethod
  867. def load(cls, result, fname="<function>"):
  868. """
  869. Convert the return value of an endpoint into a Response.
  870. :param result: The endpoint return value to load the Response from.
  871. :type result: Union[Response, werkzeug.wrappers.BaseResponse,
  872. werkzeug.exceptions.HTTPException, str, bytes, NoneType]
  873. :param str fname: The endpoint function name wherefrom the
  874. result emanated, used for logging.
  875. :returns: The created :class:`~odoo.http.Response`.
  876. :rtype: Response
  877. :raises TypeError: When ``result`` type is none of the above-
  878. mentioned type.
  879. """
  880. if isinstance(result, Response):
  881. return result
  882. if isinstance(result, werkzeug.exceptions.HTTPException):
  883. _logger.warning("%s returns an HTTPException instead of raising it.", fname)
  884. raise result
  885. if isinstance(result, werkzeug.wrappers.Response):
  886. response = cls.force_type(result)
  887. response.set_default()
  888. return response
  889. if isinstance(result, (bytes, str, type(None))):
  890. return cls(result)
  891. raise TypeError(f"{fname} returns an invalid value: {result}")
  892. def set_default(self, template=None, qcontext=None, uid=None):
  893. self.template = template
  894. self.qcontext = qcontext or dict()
  895. self.qcontext['response_template'] = self.template
  896. self.uid = uid
  897. @property
  898. def is_qweb(self):
  899. return self.template is not None
  900. def render(self):
  901. """ Renders the Response's template, returns the result. """
  902. self.qcontext['request'] = request
  903. return request.env["ir.ui.view"]._render_template(self.template, self.qcontext)
  904. def flatten(self):
  905. """
  906. Forces the rendering of the response's template, sets the result
  907. as response body and unsets :attr:`.template`
  908. """
  909. if self.template:
  910. self.response.append(self.render())
  911. self.template = None
  912. def set_cookie(self, key, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite=None, cookie_type='required'):
  913. if request.db and not request.env['ir.http']._is_allowed_cookie(cookie_type):
  914. expires = 0
  915. max_age = 0
  916. super().set_cookie(key, value=value, max_age=max_age, expires=expires, path=path, domain=domain, secure=secure, httponly=httponly, samesite=samesite)
  917. class FutureResponse:
  918. """
  919. werkzeug.Response mock class that only serves as placeholder for
  920. headers to be injected in the final response.
  921. """
  922. # used by werkzeug.Response.set_cookie
  923. charset = 'utf-8'
  924. max_cookie_size = 4093
  925. def __init__(self):
  926. self.headers = werkzeug.datastructures.Headers()
  927. @property
  928. def _charset(self):
  929. return self.charset
  930. @functools.wraps(werkzeug.Response.set_cookie)
  931. def set_cookie(self, key, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite=None, cookie_type='required'):
  932. if request.db and not request.env['ir.http']._is_allowed_cookie(cookie_type):
  933. expires = 0
  934. max_age = 0
  935. werkzeug.Response.set_cookie(self, key, value=value, max_age=max_age, expires=expires, path=path, domain=domain, secure=secure, httponly=httponly, samesite=samesite)
  936. class Request:
  937. """
  938. Wrapper around the incoming HTTP request with deserialized request
  939. parameters, session utilities and request dispatching logic.
  940. """
  941. def __init__(self, httprequest):
  942. self.httprequest = httprequest
  943. self.future_response = FutureResponse()
  944. self.dispatcher = _dispatchers['http'](self) # until we match
  945. #self.params = {} # set by the Dispatcher
  946. self.registry = None
  947. self.env = None
  948. def _post_init(self):
  949. self.session, self.db = self._get_session_and_dbname()
  950. def _get_session_and_dbname(self):
  951. # The session is explicit when it comes from the query-string or
  952. # the header. It is implicit when it comes from the cookie or
  953. # that is does not exist yet. The explicit session should be
  954. # used in this request only, it should not be saved on the
  955. # response cookie.
  956. sid = (self.httprequest.args.get('session_id')
  957. or self.httprequest.headers.get("X-Openerp-Session-Id"))
  958. if sid:
  959. is_explicit = True
  960. else:
  961. sid = self.httprequest.cookies.get('session_id')
  962. is_explicit = False
  963. if sid is None:
  964. session = root.session_store.new()
  965. else:
  966. session = root.session_store.get(sid)
  967. session.sid = sid # in case the session was not persisted
  968. session.is_explicit = is_explicit
  969. for key, val in get_default_session().items():
  970. session.setdefault(key, val)
  971. if not session.context.get('lang'):
  972. session.context['lang'] = self.default_lang()
  973. dbname = None
  974. host = self.httprequest.environ['HTTP_HOST']
  975. if session.db and db_filter([session.db], host=host):
  976. dbname = session.db
  977. else:
  978. all_dbs = db_list(force=True, host=host)
  979. if len(all_dbs) == 1:
  980. dbname = all_dbs[0] # monodb
  981. if session.db != dbname:
  982. if session.db:
  983. _logger.warning("Logged into database %r, but dbfilter rejects it; logging session out.", session.db)
  984. session.logout(keep_db=False)
  985. session.db = dbname
  986. session.is_dirty = False
  987. return session, dbname
  988. # =====================================================
  989. # Getters and setters
  990. # =====================================================
  991. def update_env(self, user=None, context=None, su=None):
  992. """ Update the environment of the current request.
  993. :param user: optional user/user id to change the current user
  994. :type user: int or :class:`res.users record<~odoo.addons.base.models.res_users.Users>`
  995. :param dict context: optional context dictionary to change the current context
  996. :param bool su: optional boolean to change the superuser mode
  997. """
  998. cr = None # None is a sentinel, it keeps the same cursor
  999. self.env = self.env(cr, user, context, su)
  1000. threading.current_thread().uid = self.env.uid
  1001. def update_context(self, **overrides):
  1002. """
  1003. Override the environment context of the current request with the
  1004. values of ``overrides``. To replace the entire context, please
  1005. use :meth:`~update_env` instead.
  1006. """
  1007. self.update_env(context=dict(self.env.context, **overrides))
  1008. @property
  1009. def context(self):
  1010. return self.env.context
  1011. @context.setter
  1012. def context(self, value):
  1013. raise NotImplementedError("Use request.update_context instead.")
  1014. @property
  1015. def uid(self):
  1016. return self.env.uid
  1017. @uid.setter
  1018. def uid(self, value):
  1019. raise NotImplementedError("Use request.update_env instead.")
  1020. @property
  1021. def cr(self):
  1022. return self.env.cr
  1023. @cr.setter
  1024. def cr(self, value):
  1025. if value is None:
  1026. raise NotImplementedError("Close the cursor instead.")
  1027. raise ValueError("You cannot replace the cursor attached to the current request.")
  1028. _cr = cr
  1029. @property
  1030. def geoip(self):
  1031. """
  1032. Get the remote address geolocalisation.
  1033. When geolocalization is successful, the return value is a
  1034. dictionary whose format is:
  1035. {'city': str, 'country_code': str, 'country_name': str,
  1036. 'latitude': float, 'longitude': float, 'region': str,
  1037. 'time_zone': str}
  1038. When geolocalization fails, an empty dict is returned.
  1039. """
  1040. if '_geoip' not in self.session:
  1041. was_dirty = self.session.is_dirty
  1042. self.session._geoip = (self.registry['ir.http']._geoip_resolve()
  1043. if self.db else self._geoip_resolve())
  1044. self.session.is_dirty = was_dirty
  1045. return self.session._geoip
  1046. @lazy_property
  1047. def best_lang(self):
  1048. lang = self.httprequest.accept_languages.best
  1049. if not lang:
  1050. return None
  1051. try:
  1052. code, territory, _, _ = babel.core.parse_locale(lang, sep='-')
  1053. if territory:
  1054. lang = f'{code}_{territory}'
  1055. else:
  1056. lang = babel.core.LOCALE_ALIASES[code]
  1057. return lang
  1058. except (ValueError, KeyError):
  1059. return None
  1060. # =====================================================
  1061. # Helpers
  1062. # =====================================================
  1063. def csrf_token(self, time_limit=None):
  1064. """
  1065. Generates and returns a CSRF token for the current session
  1066. :param Optional[int] time_limit: the CSRF token should only be
  1067. valid for the specified duration (in second), by default
  1068. 48h, ``None`` for the token to be valid as long as the
  1069. current user's session is.
  1070. :returns: ASCII token string
  1071. :rtype: str
  1072. """
  1073. secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
  1074. if not secret:
  1075. raise ValueError("CSRF protection requires a configured database secret")
  1076. # if no `time_limit` => distant 1y expiry so max_ts acts as salt, e.g. vs BREACH
  1077. max_ts = int(time.time() + (time_limit or CSRF_TOKEN_SALT))
  1078. msg = f'{self.session.sid}{max_ts}'.encode('utf-8')
  1079. hm = hmac.new(secret.encode('ascii'), msg, hashlib.sha1).hexdigest()
  1080. return f'{hm}o{max_ts}'
  1081. def validate_csrf(self, csrf):
  1082. """
  1083. Is the given csrf token valid ?
  1084. :param str csrf: The token to validate.
  1085. :returns: ``True`` when valid, ``False`` when not.
  1086. :rtype: bool
  1087. """
  1088. if not csrf:
  1089. return False
  1090. secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
  1091. if not secret:
  1092. raise ValueError("CSRF protection requires a configured database secret")
  1093. hm, _, max_ts = csrf.rpartition('o')
  1094. msg = f'{self.session.sid}{max_ts}'.encode('utf-8')
  1095. if max_ts:
  1096. try:
  1097. if int(max_ts) < int(time.time()):
  1098. return False
  1099. except ValueError:
  1100. return False
  1101. hm_expected = hmac.new(secret.encode('ascii'), msg, hashlib.sha1).hexdigest()
  1102. return consteq(hm, hm_expected)
  1103. def default_context(self):
  1104. return dict(get_default_session()['context'], lang=self.default_lang())
  1105. def default_lang(self):
  1106. """Returns default user language according to request specification
  1107. :returns: Preferred language if specified or 'en_US'
  1108. :rtype: str
  1109. """
  1110. return self.best_lang or DEFAULT_LANG
  1111. def _geoip_resolve(self):
  1112. if not (root.geoip_resolver and self.httprequest.remote_addr):
  1113. return {}
  1114. return root.geoip_resolver.resolve(self.httprequest.remote_addr) or {}
  1115. def get_http_params(self):
  1116. """
  1117. Extract key=value pairs from the query string and the forms
  1118. present in the body (both application/x-www-form-urlencoded and
  1119. multipart/form-data).
  1120. :returns: The merged key-value pairs.
  1121. :rtype: dict
  1122. """
  1123. params = {
  1124. **self.httprequest.args,
  1125. **self.httprequest.form,
  1126. **self.httprequest.files
  1127. }
  1128. params.pop('session_id', None)
  1129. return params
  1130. def get_json_data(self):
  1131. return json.loads(self.httprequest.get_data(as_text=True))
  1132. def _get_profiler_context_manager(self):
  1133. """
  1134. Get a profiler when the profiling is enabled and the requested
  1135. URL is profile-safe. Otherwise, get a context-manager that does
  1136. nothing.
  1137. """
  1138. if self.session.profile_session and self.db:
  1139. if self.session.profile_expiration < str(datetime.now()):
  1140. # avoid having session profiling for too long if user forgets to disable profiling
  1141. self.session.profile_session = None
  1142. _logger.warning("Profiling expiration reached, disabling profiling")
  1143. elif 'set_profiling' in self.httprequest.path:
  1144. _logger.debug("Profiling disabled on set_profiling route")
  1145. elif self.httprequest.path.startswith('/websocket'):
  1146. _logger.debug("Profiling disabled for websocket")
  1147. elif odoo.evented:
  1148. # only longpolling should be in a evented server, but this is an additional safety
  1149. _logger.debug("Profiling disabled for evented server")
  1150. else:
  1151. try:
  1152. return profiler.Profiler(
  1153. db=self.db,
  1154. description=self.httprequest.full_path,
  1155. profile_session=self.session.profile_session,
  1156. collectors=self.session.profile_collectors,
  1157. params=self.session.profile_params,
  1158. )
  1159. except Exception:
  1160. _logger.exception("Failure during Profiler creation")
  1161. self.session.profile_session = None
  1162. return contextlib.nullcontext()
  1163. def _inject_future_response(self, response):
  1164. response.headers.extend(self.future_response.headers)
  1165. return response
  1166. def make_response(self, data, headers=None, cookies=None, status=200):
  1167. """ Helper for non-HTML responses, or HTML responses with custom
  1168. response headers or cookies.
  1169. While handlers can just return the HTML markup of a page they want to
  1170. send as a string if non-HTML data is returned they need to create a
  1171. complete response object, or the returned data will not be correctly
  1172. interpreted by the clients.
  1173. :param str data: response body
  1174. :param int status: http status code
  1175. :param headers: HTTP headers to set on the response
  1176. :type headers: ``[(name, value)]``
  1177. :param collections.abc.Mapping cookies: cookies to set on the client
  1178. :returns: a response object.
  1179. :rtype: :class:`~odoo.http.Response`
  1180. """
  1181. response = Response(data, status=status, headers=headers)
  1182. if cookies:
  1183. for k, v in cookies.items():
  1184. response.set_cookie(k, v)
  1185. return response
  1186. def make_json_response(self, data, headers=None, cookies=None, status=200):
  1187. """ Helper for JSON responses, it json-serializes ``data`` and
  1188. sets the Content-Type header accordingly if none is provided.
  1189. :param data: the data that will be json-serialized into the response body
  1190. :param int status: http status code
  1191. :param List[(str, str)] headers: HTTP headers to set on the response
  1192. :param collections.abc.Mapping cookies: cookies to set on the client
  1193. :rtype: :class:`~odoo.http.Response`
  1194. """
  1195. data = json.dumps(data, ensure_ascii=False, default=date_utils.json_default)
  1196. headers = werkzeug.datastructures.Headers(headers)
  1197. headers['Content-Length'] = len(data)
  1198. if 'Content-Type' not in headers:
  1199. headers['Content-Type'] = 'application/json; charset=utf-8'
  1200. return self.make_response(data, headers.to_wsgi_list(), cookies, status)
  1201. def not_found(self, description=None):
  1202. """ Shortcut for a `HTTP 404
  1203. <http://tools.ietf.org/html/rfc7231#section-6.5.4>`_ (Not Found)
  1204. response
  1205. """
  1206. return NotFound(description)
  1207. def redirect(self, location, code=303, local=True):
  1208. # compatibility, Werkzeug support URL as location
  1209. if isinstance(location, URL):
  1210. location = location.to_url()
  1211. if local:
  1212. location = '/' + url_parse(location).replace(scheme='', netloc='').to_url().lstrip('/')
  1213. if self.db:
  1214. return self.env['ir.http']._redirect(location, code)
  1215. return werkzeug.utils.redirect(location, code, Response=Response)
  1216. def redirect_query(self, location, query=None, code=303, local=True):
  1217. if query:
  1218. location += '?' + url_encode(query)
  1219. return self.redirect(location, code=code, local=local)
  1220. def render(self, template, qcontext=None, lazy=True, **kw):
  1221. """ Lazy render of a QWeb template.
  1222. The actual rendering of the given template will occur at then end of
  1223. the dispatching. Meanwhile, the template and/or qcontext can be
  1224. altered or even replaced by a static response.
  1225. :param str template: template to render
  1226. :param dict qcontext: Rendering context to use
  1227. :param bool lazy: whether the template rendering should be deferred
  1228. until the last possible moment
  1229. :param dict kw: forwarded to werkzeug's Response object
  1230. """
  1231. response = Response(template=template, qcontext=qcontext, **kw)
  1232. if not lazy:
  1233. return response.render()
  1234. return response
  1235. def _save_session(self):
  1236. """ Save a modified session on disk. """
  1237. sess = self.session
  1238. if not sess.can_save:
  1239. return
  1240. if sess.should_rotate:
  1241. sess['_geoip'] = self.geoip
  1242. root.session_store.rotate(sess, self.env) # it saves
  1243. elif sess.is_dirty:
  1244. sess['_geoip'] = self.geoip
  1245. root.session_store.save(sess)
  1246. # We must not set the cookie if the session id was specified
  1247. # using a http header or a GET parameter.
  1248. # There are two reasons to this:
  1249. # - When using one of those two means we consider that we are
  1250. # overriding the cookie, which means creating a new session on
  1251. # top of an already existing session and we don't want to
  1252. # create a mess with the 'normal' session (the one using the
  1253. # cookie). That is a special feature of the Javascript Session.
  1254. # - It could allow session fixation attacks.
  1255. cookie_sid = self.httprequest.cookies.get('session_id')
  1256. if not sess.is_explicit and (sess.is_dirty or cookie_sid != sess.sid):
  1257. self.future_response.set_cookie('session_id', sess.sid, max_age=SESSION_LIFETIME, httponly=True)
  1258. def _set_request_dispatcher(self, rule):
  1259. routing = rule.endpoint.routing
  1260. dispatcher_cls = _dispatchers[routing['type']]
  1261. if (not is_cors_preflight(self, rule.endpoint)
  1262. and not dispatcher_cls.is_compatible_with(self)):
  1263. compatible_dispatchers = [
  1264. disp.routing_type
  1265. for disp in _dispatchers.values()
  1266. if disp.is_compatible_with(self)
  1267. ]
  1268. raise BadRequest(f"Request inferred type is compatible with {compatible_dispatchers} but {routing['routes'][0]!r} is type={routing['type']!r}.")
  1269. self.dispatcher = dispatcher_cls(self)
  1270. # =====================================================
  1271. # Routing
  1272. # =====================================================
  1273. def _serve_static(self):
  1274. """ Serve a static file from the file system. """
  1275. module, _, path = self.httprequest.path[1:].partition('/static/')
  1276. try:
  1277. directory = root.statics[module]
  1278. filepath = werkzeug.security.safe_join(directory, path)
  1279. return Stream.from_path(filepath).get_response(
  1280. max_age=0 if 'assets' in self.session.debug else STATIC_CACHE,
  1281. )
  1282. except KeyError:
  1283. raise NotFound(f'Module "{module}" not found.\n')
  1284. except OSError: # cover both missing file and invalid permissions
  1285. raise NotFound(f'File "{path}" not found in module {module}.\n')
  1286. def _serve_nodb(self):
  1287. """
  1288. Dispatch the request to its matching controller in a
  1289. database-free environment.
  1290. """
  1291. router = root.nodb_routing_map.bind_to_environ(self.httprequest.environ)
  1292. rule, args = router.match(return_rule=True)
  1293. self._set_request_dispatcher(rule)
  1294. self.dispatcher.pre_dispatch(rule, args)
  1295. response = self.dispatcher.dispatch(rule.endpoint, args)
  1296. self.dispatcher.post_dispatch(response)
  1297. return response
  1298. def _serve_db(self):
  1299. """
  1300. Prepare the user session and load the ORM before forwarding the
  1301. request to ``_serve_ir_http``.
  1302. """
  1303. try:
  1304. self.registry = Registry(self.db).check_signaling()
  1305. except (AttributeError, psycopg2.OperationalError, psycopg2.ProgrammingError):
  1306. # psycopg2 error or attribute error while constructing
  1307. # the registry. That means either
  1308. # - the database probably does not exists anymore, or
  1309. # - the database is corrupted, or
  1310. # - the database version doesn't match the server version.
  1311. # So remove the database from the cookie
  1312. self.db = None
  1313. self.session.db = None
  1314. root.session_store.save(self.session)
  1315. if request.httprequest.path == '/web':
  1316. # Internal Server Error
  1317. raise
  1318. else:
  1319. return self._serve_nodb()
  1320. with contextlib.closing(self.registry.cursor()) as cr:
  1321. self.env = odoo.api.Environment(cr, self.session.uid, self.session.context)
  1322. threading.current_thread().uid = self.env.uid
  1323. try:
  1324. return service_model.retrying(self._serve_ir_http, self.env)
  1325. except Exception as exc:
  1326. if isinstance(exc, HTTPException) and exc.code is None:
  1327. raise # bubble up to odoo.http.Application.__call__
  1328. exc.error_response = self.registry['ir.http']._handle_error(exc)
  1329. raise
  1330. def _serve_ir_http(self):
  1331. """
  1332. Delegate most of the processing to the ir.http model that is
  1333. extensible by applications.
  1334. """
  1335. ir_http = self.registry['ir.http']
  1336. try:
  1337. rule, args = ir_http._match(self.httprequest.path)
  1338. except NotFound:
  1339. self.params = self.get_http_params()
  1340. response = ir_http._serve_fallback()
  1341. if response:
  1342. self.dispatcher.post_dispatch(response)
  1343. return response
  1344. raise
  1345. self._set_request_dispatcher(rule)
  1346. ir_http._authenticate(rule.endpoint)
  1347. ir_http._pre_dispatch(rule, args)
  1348. response = self.dispatcher.dispatch(rule.endpoint, args)
  1349. # the registry can have been reniewed by dispatch
  1350. self.registry['ir.http']._post_dispatch(response)
  1351. return response
  1352. # =========================================================
  1353. # Core type-specialized dispatchers
  1354. # =========================================================
  1355. _dispatchers = {}
  1356. class Dispatcher(ABC):
  1357. routing_type: str
  1358. @classmethod
  1359. def __init_subclass__(cls):
  1360. super().__init_subclass__()
  1361. _dispatchers[cls.routing_type] = cls
  1362. def __init__(self, request):
  1363. self.request = request
  1364. @classmethod
  1365. @abstractmethod
  1366. def is_compatible_with(cls, request):
  1367. """
  1368. Determine if the current request is compatible with this
  1369. dispatcher.
  1370. """
  1371. def pre_dispatch(self, rule, args):
  1372. """
  1373. Prepare the system before dispatching the request to its
  1374. controller. This method is often overridden in ir.http to
  1375. extract some info from the request query-string or headers and
  1376. to save them in the session or in the context.
  1377. """
  1378. routing = rule.endpoint.routing
  1379. self.request.session.can_save = routing.get('save_session', True)
  1380. set_header = self.request.future_response.headers.set
  1381. cors = routing.get('cors')
  1382. if cors:
  1383. set_header('Access-Control-Allow-Origin', cors)
  1384. set_header('Access-Control-Allow-Methods', (
  1385. 'POST' if routing['type'] == 'json'
  1386. else ', '.join(routing['methods'] or ['GET', 'POST'])
  1387. ))
  1388. if cors and self.request.httprequest.method == 'OPTIONS':
  1389. set_header('Access-Control-Max-Age', CORS_MAX_AGE)
  1390. set_header('Access-Control-Allow-Headers',
  1391. 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
  1392. werkzeug.exceptions.abort(Response(status=204))
  1393. @abstractmethod
  1394. def dispatch(self, endpoint, args):
  1395. """
  1396. Extract the params from the request's body and call the
  1397. endpoint. While it is preferred to override ir.http._pre_dispatch
  1398. and ir.http._post_dispatch, this method can be override to have
  1399. a tight control over the dispatching.
  1400. """
  1401. def post_dispatch(self, response):
  1402. """
  1403. Manipulate the HTTP response to inject various headers, also
  1404. save the session when it is dirty.
  1405. """
  1406. self.request._save_session()
  1407. self.request._inject_future_response(response)
  1408. root.set_csp(response)
  1409. @abstractmethod
  1410. def handle_error(self, exc: Exception) -> collections.abc.Callable:
  1411. """
  1412. Transform the exception into a valid HTTP response. Called upon
  1413. any exception while serving a request.
  1414. """
  1415. class HttpDispatcher(Dispatcher):
  1416. routing_type = 'http'
  1417. @classmethod
  1418. def is_compatible_with(cls, request):
  1419. return True
  1420. def dispatch(self, endpoint, args):
  1421. """
  1422. Perform http-related actions such as deserializing the request
  1423. body and query-string and checking cors/csrf while dispatching a
  1424. request to a ``type='http'`` route.
  1425. See :meth:`~odoo.http.Response.load` method for the compatible
  1426. endpoint return types.
  1427. """
  1428. self.request.params = dict(self.request.get_http_params(), **args)
  1429. # Check for CSRF token for relevant requests
  1430. if self.request.httprequest.method not in CSRF_FREE_METHODS and endpoint.routing.get('csrf', True):
  1431. if not self.request.db:
  1432. return self.request.redirect('/web/database/selector')
  1433. token = self.request.params.pop('csrf_token', None)
  1434. if not self.request.validate_csrf(token):
  1435. if token is not None:
  1436. _logger.warning("CSRF validation failed on path '%s'", self.request.httprequest.path)
  1437. else:
  1438. _logger.warning(MISSING_CSRF_WARNING, request.httprequest.path)
  1439. raise werkzeug.exceptions.BadRequest('Session expired (invalid CSRF token)')
  1440. if self.request.db:
  1441. return self.request.registry['ir.http']._dispatch(endpoint)
  1442. else:
  1443. return endpoint(**self.request.params)
  1444. def handle_error(self, exc: Exception) -> collections.abc.Callable:
  1445. """
  1446. Handle any exception that occurred while dispatching a request
  1447. to a `type='http'` route. Also handle exceptions that occurred
  1448. when no route matched the request path, when no fallback page
  1449. could be delivered and that the request ``Content-Type`` was not
  1450. json.
  1451. :param Exception exc: the exception that occurred.
  1452. :returns: a WSGI application
  1453. """
  1454. if isinstance(exc, SessionExpiredException):
  1455. session = self.request.session
  1456. was_connected = session.uid is not None
  1457. session.logout(keep_db=True)
  1458. response = self.request.redirect_query('/web/login', {'redirect': self.request.httprequest.full_path})
  1459. if not session.is_explicit and was_connected:
  1460. root.session_store.rotate(session, self.request.env)
  1461. response.set_cookie('session_id', session.sid, max_age=SESSION_LIFETIME, httponly=True)
  1462. return response
  1463. return (exc if isinstance(exc, HTTPException)
  1464. else Forbidden(exc.args[0]) if isinstance(exc, (AccessDenied, AccessError))
  1465. else BadRequest(exc.args[0]) if isinstance(exc, UserError)
  1466. else InternalServerError() # hide the real error
  1467. )
  1468. class JsonRPCDispatcher(Dispatcher):
  1469. routing_type = 'json'
  1470. def __init__(self, request):
  1471. super().__init__(request)
  1472. self.jsonrequest = {}
  1473. self.request_id = None
  1474. @classmethod
  1475. def is_compatible_with(cls, request):
  1476. return request.httprequest.mimetype in JSON_MIMETYPES
  1477. def dispatch(self, endpoint, args):
  1478. """
  1479. `JSON-RPC 2 <http://www.jsonrpc.org/specification>`_ over HTTP.
  1480. Our implementation differs from the specification on two points:
  1481. 1. The ``method`` member of the JSON-RPC request payload is
  1482. ignored as the HTTP path is already used to route the request
  1483. to the controller.
  1484. 2. We only support parameter structures by-name, i.e. the
  1485. ``params`` member of the JSON-RPC request payload MUST be a
  1486. JSON Object and not a JSON Array.
  1487. In addition, it is possible to pass a context that replaces
  1488. the session context via a special ``context`` argument that is
  1489. removed prior to calling the endpoint.
  1490. Successful request::
  1491. --> {"jsonrpc": "2.0", "method": "call", "params": {"context": {}, "arg1": "val1" }, "id": null}
  1492. <-- {"jsonrpc": "2.0", "result": { "res1": "val1" }, "id": null}
  1493. Request producing a error::
  1494. --> {"jsonrpc": "2.0", "method": "call", "params": {"context": {}, "arg1": "val1" }, "id": null}
  1495. <-- {"jsonrpc": "2.0", "error": {"code": 1, "message": "End user error message.", "data": {"code": "codestring", "debug": "traceback" } }, "id": null}
  1496. """
  1497. try:
  1498. self.jsonrequest = self.request.get_json_data()
  1499. self.request_id = self.jsonrequest.get('id')
  1500. except ValueError as exc:
  1501. # must use abort+Response to bypass handle_error
  1502. werkzeug.exceptions.abort(Response("Invalid JSON data", status=400))
  1503. except AttributeError as exc:
  1504. # must use abort+Response to bypass handle_error
  1505. werkzeug.exceptions.abort(Response("Invalid JSON-RPC data", status=400))
  1506. self.request.params = dict(self.jsonrequest.get('params', {}), **args)
  1507. ctx = self.request.params.pop('context', None)
  1508. if ctx is not None and self.request.db:
  1509. self.request.update_context(**ctx)
  1510. if self.request.db:
  1511. result = self.request.registry['ir.http']._dispatch(endpoint)
  1512. else:
  1513. result = endpoint(**self.request.params)
  1514. return self._response(result)
  1515. def handle_error(self, exc: Exception) -> collections.abc.Callable:
  1516. """
  1517. Handle any exception that occurred while dispatching a request to
  1518. a `type='json'` route. Also handle exceptions that occurred when
  1519. no route matched the request path, that no fallback page could
  1520. be delivered and that the request ``Content-Type`` was json.
  1521. :param exc: the exception that occurred.
  1522. :returns: a WSGI application
  1523. """
  1524. error = {
  1525. 'code': 200, # this code is the JSON-RPC level code, it is
  1526. # distinct from the HTTP status code. This
  1527. # code is ignored and the value 200 (while
  1528. # misleading) is totally arbitrary.
  1529. 'message': "Odoo Server Error",
  1530. 'data': serialize_exception(exc),
  1531. }
  1532. if isinstance(exc, NotFound):
  1533. error['code'] = 404
  1534. error['message'] = "404: Not Found"
  1535. elif isinstance(exc, SessionExpiredException):
  1536. error['code'] = 100
  1537. error['message'] = "Odoo Session Expired"
  1538. return self._response(error=error)
  1539. def _response(self, result=None, error=None):
  1540. response = {'jsonrpc': '2.0', 'id': self.request_id}
  1541. if error is not None:
  1542. response['error'] = error
  1543. if result is not None:
  1544. response['result'] = result
  1545. return self.request.make_json_response(response)
  1546. # =========================================================
  1547. # WSGI Entry Point
  1548. # =========================================================
  1549. class Application:
  1550. """ Odoo WSGI application """
  1551. # See also: https://www.python.org/dev/peps/pep-3333
  1552. @lazy_property
  1553. def statics(self):
  1554. """
  1555. Map module names to their absolute ``static`` path on the file
  1556. system.
  1557. """
  1558. mod2path = {}
  1559. for addons_path in odoo.addons.__path__:
  1560. for module in os.listdir(addons_path):
  1561. manifest = get_manifest(module)
  1562. static_path = opj(addons_path, module, 'static')
  1563. if (manifest
  1564. and (manifest['installable'] or manifest['assets'])
  1565. and os.path.isdir(static_path)):
  1566. mod2path[module] = static_path
  1567. return mod2path
  1568. def get_static_file(self, url, host=''):
  1569. """
  1570. Get the full-path of the file if the url resolves to a local
  1571. static file, otherwise return None.
  1572. Without the second host parameters, ``url`` must be an absolute
  1573. path, others URLs are considered faulty.
  1574. With the second host parameters, ``url`` can also be a full URI
  1575. and the authority found in the URL (if any) is validated against
  1576. the given ``host``.
  1577. """
  1578. netloc, path = urlparse(url)[1:3]
  1579. try:
  1580. path_netloc, module, static, resource = path.split('/', 3)
  1581. except ValueError:
  1582. return None
  1583. if ((netloc and netloc != host) or (path_netloc and path_netloc != host)):
  1584. return None
  1585. if (module not in self.statics or static != 'static' or not resource):
  1586. return None
  1587. try:
  1588. return file_path(f'{module}/static/{resource}')
  1589. except FileNotFoundError:
  1590. return None
  1591. @lazy_property
  1592. def nodb_routing_map(self):
  1593. nodb_routing_map = werkzeug.routing.Map(strict_slashes=False, converters=None)
  1594. for url, endpoint in _generate_routing_rules([''] + odoo.conf.server_wide_modules, nodb_only=True):
  1595. routing = submap(endpoint.routing, ROUTING_KEYS)
  1596. if routing['methods'] is not None and 'OPTIONS' not in routing['methods']:
  1597. routing['methods'] = routing['methods'] + ['OPTIONS']
  1598. rule = werkzeug.routing.Rule(url, endpoint=endpoint, **routing)
  1599. rule.merge_slashes = False
  1600. nodb_routing_map.add(rule)
  1601. return nodb_routing_map
  1602. @lazy_property
  1603. def session_store(self):
  1604. path = odoo.tools.config.session_dir
  1605. _logger.debug('HTTP sessions stored in: %s', path)
  1606. return FilesystemSessionStore(path, session_class=Session, renew_missing=True)
  1607. @lazy_property
  1608. def geoip_resolver(self):
  1609. try:
  1610. return GeoIPResolver.open(config.get('geoip_database'))
  1611. except Exception as e:
  1612. _logger.warning('Cannot load GeoIP: %s', e)
  1613. def get_db_router(self, db):
  1614. if not db:
  1615. return self.nodb_routing_map
  1616. return request.registry['ir.http'].routing_map()
  1617. def set_csp(self, response):
  1618. headers = response.headers
  1619. if 'Content-Security-Policy' in headers:
  1620. return
  1621. mime, _params = cgi.parse_header(headers.get('Content-Type', ''))
  1622. if not mime.startswith('image/'):
  1623. return
  1624. headers['Content-Security-Policy'] = "default-src 'none'"
  1625. headers['X-Content-Type-Options'] = 'nosniff'
  1626. def __call__(self, environ, start_response):
  1627. """
  1628. WSGI application entry point.
  1629. :param dict environ: container for CGI environment variables
  1630. such as the request HTTP headers, the source IP address and
  1631. the body as an io file.
  1632. :param callable start_response: function provided by the WSGI
  1633. server that this application must call in order to send the
  1634. HTTP response status line and the response headers.
  1635. """
  1636. current_thread = threading.current_thread()
  1637. current_thread.query_count = 0
  1638. current_thread.query_time = 0
  1639. current_thread.perf_t0 = time.time()
  1640. if hasattr(current_thread, 'dbname'):
  1641. del current_thread.dbname
  1642. if hasattr(current_thread, 'uid'):
  1643. del current_thread.uid
  1644. if odoo.tools.config['proxy_mode'] and environ.get("HTTP_X_FORWARDED_HOST"):
  1645. # The ProxyFix middleware has a side effect of updating the
  1646. # environ, see https://github.com/pallets/werkzeug/pull/2184
  1647. def fake_app(environ, start_response):
  1648. return []
  1649. def fake_start_response(status, headers):
  1650. return
  1651. ProxyFix(fake_app)(environ, fake_start_response)
  1652. httprequest = werkzeug.wrappers.Request(environ)
  1653. httprequest.user_agent_class = UserAgent # use vendored userAgent since it will be removed in 2.1
  1654. httprequest.parameter_storage_class = (
  1655. werkzeug.datastructures.ImmutableOrderedMultiDict)
  1656. request = Request(httprequest)
  1657. _request_stack.push(request)
  1658. request._post_init()
  1659. current_thread.url = httprequest.url
  1660. try:
  1661. if self.get_static_file(httprequest.path):
  1662. response = request._serve_static()
  1663. elif request.db:
  1664. with request._get_profiler_context_manager():
  1665. response = request._serve_db()
  1666. else:
  1667. response = request._serve_nodb()
  1668. return response(environ, start_response)
  1669. except Exception as exc:
  1670. # Valid (2xx/3xx) response returned via werkzeug.exceptions.abort.
  1671. if isinstance(exc, HTTPException) and exc.code is None:
  1672. response = exc.get_response()
  1673. HttpDispatcher(request).post_dispatch(response)
  1674. return response(environ, start_response)
  1675. # Logs the error here so the traceback starts with ``__call__``.
  1676. if hasattr(exc, 'loglevel'):
  1677. _logger.log(exc.loglevel, exc, exc_info=getattr(exc, 'exc_info', None))
  1678. elif isinstance(exc, HTTPException):
  1679. pass
  1680. elif isinstance(exc, SessionExpiredException):
  1681. _logger.info(exc)
  1682. elif isinstance(exc, (UserError, AccessError, NotFound)):
  1683. _logger.warning(exc)
  1684. else:
  1685. _logger.error("Exception during request handling.", exc_info=True)
  1686. # Ensure there is always a WSGI handler attached to the exception.
  1687. if not hasattr(exc, 'error_response'):
  1688. exc.error_response = request.dispatcher.handle_error(exc)
  1689. return exc.error_response(environ, start_response)
  1690. finally:
  1691. _request_stack.pop()
  1692. root = Application()