diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py index b88a833601bcee7a2b1d074e95f1c9b6e98bdaee..9ec7b0db526f0a5225707717762ede4f69ba0415 100644 --- a/addons/bus/models/bus.py +++ b/addons/bus/models/bus.py @@ -99,6 +99,7 @@ class ImDispatch(object): def __init__(self): self.channels = {} self.started = False + self.Event = None def poll(self, dbname, channels, last, options=None, timeout=TIMEOUT): if options is None: @@ -181,7 +182,7 @@ class ImDispatch(object): def start(self): if odoo.evented: # gevent mode - import gevent + import gevent.event # pylint: disable=import-outside-toplevel self.Event = gevent.event.Event gevent.spawn(self.run) else: diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py index 96127e5636740ff1048492e1705ada7d5df349db..01b5b77a19e951407a7ff011e0ffc06f3c1dd54a 100644 --- a/addons/http_routing/models/ir_http.py +++ b/addons/http_routing/models/ir_http.py @@ -528,8 +528,13 @@ class IrHttp(models.AbstractModel): raise Exception("Rerouting limit exceeded") request.httprequest.environ['PATH_INFO'] = path # void werkzeug cached_property. TODO: find a proper way to do this - for key in ('path', 'full_path', 'url', 'base_url'): + for key in ('full_path', 'url', 'base_url'): request.httprequest.__dict__.pop(key, None) + # since werkzeug 2.0 `path`` became an attribute and is not a cached property anymore + if hasattr(type(request.httprequest), 'path'): # cached property + request.httprequest.__dict__.pop('path', None) + else: # direct attribute + request.httprequest.path = '/' + path.lstrip('/') return cls._dispatch() diff --git a/addons/link_tracker/models/mail_render_mixin.py b/addons/link_tracker/models/mail_render_mixin.py index 3fa3159f361b0907cfaebd8d852de6f233837a13..21ba81f4d90a4c6f9c9a69ae8815f011b17e2dda 100644 --- a/addons/link_tracker/models/mail_render_mixin.py +++ b/addons/link_tracker/models/mail_render_mixin.py @@ -3,7 +3,8 @@ import re -from werkzeug import urls, utils +from html import unescape +from werkzeug import urls from odoo import api, models, tools @@ -40,7 +41,7 @@ class MailRenderMixin(models.AbstractModel): label = (match[3] or '').strip() if not blacklist or not [s for s in blacklist if s in long_url] and not long_url.startswith(short_schema): - create_vals = dict(link_tracker_vals, url=utils.unescape(long_url), label=utils.unescape(label)) + create_vals = dict(link_tracker_vals, url=unescape(long_url), label=unescape(label)) link = self.env['link.tracker'].create(create_vals) if link.short_url: new_href = href.replace(long_url, link.short_url) @@ -69,7 +70,7 @@ class MailRenderMixin(models.AbstractModel): if blacklist and any(item in parsed.path for item in blacklist): continue - create_vals = dict(link_tracker_vals, url= utils.unescape(original_url)) + create_vals = dict(link_tracker_vals, url=unescape(original_url)) link = self.env['link.tracker'].create(create_vals) if link.short_url: content = content.replace(original_url, link.short_url, 1) diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py index c30d0c645149dbc965f1cacfe75dba62de6b672d..23457bc025e51a5791b26fc85a1d56b25112a576 100644 --- a/addons/pos_mercury/models/pos_mercury_transaction.py +++ b/addons/pos_mercury/models/pos_mercury_transaction.py @@ -4,7 +4,8 @@ from datetime import date, timedelta import requests -import werkzeug + +from html import unescape from odoo import models, api, service from odoo.tools.translate import _ @@ -69,7 +70,7 @@ class MercuryTransaction(models.Model): try: r = requests.post(url, data=xml_transaction, headers=headers, timeout=65) r.raise_for_status() - response = werkzeug.utils.unescape(r.content.decode()) + response = unescape(r.content.decode()) except Exception: response = "timeout" diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 813a0d941d84bd6698749894451228827da5550c..cdd8daacd44ff7797a3bc6af454448f0fe090352 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1099,10 +1099,9 @@ class Proxy(http.Controller): if not data: raise werkzeug.exceptions.BadRequest() from werkzeug.test import Client - from werkzeug.wrappers import BaseResponse base_url = request.httprequest.base_url query_string = request.httprequest.query_string - client = Client(http.root, BaseResponse) + client = Client(http.root, werkzeug.wrappers.Response) headers = {'X-Openerp-Session-Id': request.session.sid} return client.post('/' + path, base_url=base_url, query_string=query_string, headers=headers, data=data) diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index a839ccde34e4b0b9402d54b9bb56458bc26b0a2b..c5da71c16cf43f951d3dbcb2fdf9a83c252289e0 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -129,7 +129,7 @@ class Website(Home): """ if not redirect and request.params.get('login_success'): if request.env['res.users'].browse(uid).has_group('base.group_user'): - redirect = b'/web?' + request.httprequest.query_string + redirect = '/web?' + request.httprequest.query_string.decode() else: redirect = '/my' return super()._login_redirect(uid, redirect=redirect) diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py index d2fdd23a8e54d613c58abd892f9531d30d365973..9f350cc45c80e00ee9c75b677b265de28ee011cc 100644 --- a/odoo/addons/base/models/ir_actions_report.py +++ b/odoo/addons/base/models/ir_actions_report.py @@ -22,11 +22,18 @@ import json from lxml import etree from contextlib import closing from distutils.version import LooseVersion -from reportlab.graphics.barcode import createBarcodeDrawing from PyPDF2 import PdfFileWriter, PdfFileReader, utils from collections import OrderedDict from collections.abc import Iterable from PIL import Image, ImageFile + +# Ignore a deprecation warning `load_module` usage in importlib from python 3.10 (Jammy) +# Catched here in order to not miss the same warning from elsewhere +import warnings +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from reportlab.graphics.barcode import createBarcodeDrawing + # Allow truncated images ImageFile.LOAD_TRUNCATED_IMAGES = True diff --git a/odoo/addons/base/models/qweb.py b/odoo/addons/base/models/qweb.py index 25fc7a832d83f877bfb69b3d51896b7bd8e85eee..957e85e94a1d862828edbec6b573511e37064ca3 100644 --- a/odoo/addons/base/models/qweb.py +++ b/odoo/addons/base/models/qweb.py @@ -14,10 +14,9 @@ from textwrap import dedent import itertools from lxml import etree, html from psycopg2.extensions import TransactionRollbackError -import werkzeug -from werkzeug.utils import escape as _escape from odoo.tools import pycompat, freehash +from odoo.tools.misc import html_escape as escape from odoo.tools.safe_eval import check_values import builtins @@ -160,9 +159,6 @@ class QWebException(Exception): def __repr__(self): return str(self) -# Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9 -escape = (lambda text: _escape(text, quote=True)) if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0') else _escape - def foreach_iterator(base_ctx, enum, name): ctx = base_ctx.copy() if not enum: diff --git a/odoo/http.py b/odoo/http.py index 551a16fe5fd8f9804d98a25b0fae21d636292d34..5536daedc15fb74442beb060956a21e7ac60bc72 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -55,8 +55,10 @@ from .tools import ustr, consteq, frozendict, pycompat, unique, date_utils from .tools.mimetypes import guess_mimetype from .tools.misc import str2bool from .tools._vendor import sessions +from .tools._vendor.useragents import UserAgent from .modules.module import module_manifest + _logger = logging.getLogger(__name__) rpc_request = logging.getLogger(__name__ + '.rpc.request') rpc_response = logging.getLogger(__name__ + '.rpc.response') @@ -537,7 +539,7 @@ def route(route=None, **kw): if isinstance(response, werkzeug.exceptions.HTTPException): response = response.get_response(request.httprequest.environ) - if isinstance(response, werkzeug.wrappers.BaseResponse): + if isinstance(response, werkzeug.wrappers.Response): response = Response.force_type(response) response.set_default() return response @@ -1451,7 +1453,7 @@ class Root(object): def set_csp(self, response): # ignore HTTP errors - if not isinstance(response, werkzeug.wrappers.BaseResponse): + if not isinstance(response, werkzeug.wrappers.Response): return headers = response.headers @@ -1471,6 +1473,7 @@ class Root(object): """ try: httprequest = werkzeug.wrappers.Request(environ) + httprequest.user_agent_class = UserAgent # use vendored userAgent since it will be removed in 2.1 httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableOrderedMultiDict current_thread = threading.current_thread() diff --git a/odoo/netsvc.py b/odoo/netsvc.py index 0e5baefae78740852a4c71700058a113e8b95951..deea2e288b28fb58ef403e5d080a34c740ac581b 100644 --- a/odoo/netsvc.py +++ b/odoo/netsvc.py @@ -127,9 +127,12 @@ def init_logger(): warnings.filterwarnings('default', category=DeprecationWarning) # ignore deprecation warnings from invalid escape (there's a ton and it's # pretty likely a super low-value signal) - warnings.filterwarnings('ignore', r'^invalid escape sequence \\.', category=DeprecationWarning) + warnings.filterwarnings('ignore', r'^invalid escape sequence \'?\\.', category=DeprecationWarning) # recordsets are both sequence and set so trigger warning despite no issue warnings.filterwarnings('ignore', r'^Sampling from a set', category=DeprecationWarning, module='odoo') + # (Jammy) distutils, currentThread, isDaemon, setDaemon future removal are properly handled in upper versions + warnings.filterwarnings('ignore', r'^The distutils package is deprecated and slated for removal', category=DeprecationWarning) + warnings.filterwarnings('ignore', r'^(currentThread|isDaemon|setDaemon)\(\) is deprecated', category=DeprecationWarning) # ignore a bunch of warnings we can't really fix ourselves for module in [ 'babel.util', # deprecated parser module, no release yet diff --git a/odoo/tools/_vendor/sessions.py b/odoo/tools/_vendor/sessions.py index c2c3a643cea7f8489c3535f10f95e516e1a0a090..4c8d8db6ef3bd1e140f68c148b76945151af8333 100644 --- a/odoo/tools/_vendor/sessions.py +++ b/odoo/tools/_vendor/sessions.py @@ -19,14 +19,13 @@ import os import re import tempfile from hashlib import sha1 -from os import path +from os import path, replace as rename from pickle import dump from pickle import HIGHEST_PROTOCOL from pickle import load from time import time from werkzeug.datastructures import CallbackDict -from werkzeug.posixemulation import rename _sha1_re = re.compile(r"^[a-f0-9]{40}$") diff --git a/odoo/tools/_vendor/useragents.py b/odoo/tools/_vendor/useragents.py new file mode 100644 index 0000000000000000000000000000000000000000..7627ccc973bf69e4b88642b8509178b33e5722bf --- /dev/null +++ b/odoo/tools/_vendor/useragents.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.useragents + ~~~~~~~~~~~~~~~~~~~ + + This module provides a helper to inspect user agent strings. This module + is far from complete but should work for most of the currently available + browsers. + + + :copyright: 2007 Pallets + :license: BSD-3-Clause + + This package was vendored in odoo in order to prevent errors with werkzeug 2.1 +""" +import re + + +class UserAgentParser(object): + """A simple user agent parser. Used by the `UserAgent`.""" + + platforms = ( + ("cros", "chromeos"), + ("iphone|ios", "iphone"), + ("ipad", "ipad"), + (r"darwin|mac|os\s*x", "macos"), + ("win", "windows"), + (r"android", "android"), + ("netbsd", "netbsd"), + ("openbsd", "openbsd"), + ("freebsd", "freebsd"), + ("dragonfly", "dragonflybsd"), + ("(sun|i86)os", "solaris"), + (r"x11|lin(\b|ux)?", "linux"), + (r"nintendo\s+wii", "wii"), + ("irix", "irix"), + ("hp-?ux", "hpux"), + ("aix", "aix"), + ("sco|unix_sv", "sco"), + ("bsd", "bsd"), + ("amiga", "amiga"), + ("blackberry|playbook", "blackberry"), + ("symbian", "symbian"), + ) + browsers = ( + ("googlebot", "google"), + ("msnbot", "msn"), + ("yahoo", "yahoo"), + ("ask jeeves", "ask"), + (r"aol|america\s+online\s+browser", "aol"), + ("opera", "opera"), + ("edge", "edge"), + ("chrome|crios", "chrome"), + ("seamonkey", "seamonkey"), + ("firefox|firebird|phoenix|iceweasel", "firefox"), + ("galeon", "galeon"), + ("safari|version", "safari"), + ("webkit", "webkit"), + ("camino", "camino"), + ("konqueror", "konqueror"), + ("k-meleon", "kmeleon"), + ("netscape", "netscape"), + (r"msie|microsoft\s+internet\s+explorer|trident/.+? rv:", "msie"), + ("lynx", "lynx"), + ("links", "links"), + ("Baiduspider", "baidu"), + ("bingbot", "bing"), + ("mozilla", "mozilla"), + ) + + _browser_version_re = r"(?:%s)[/\sa-z(]*(\d+[.\da-z]+)?" + _language_re = re.compile( + r"(?:;\s*|\s+)(\b\w{2}\b(?:-\b\w{2}\b)?)\s*;|" + r"(?:\(|\[|;)\s*(\b\w{2}\b(?:-\b\w{2}\b)?)\s*(?:\]|\)|;)" + ) + + def __init__(self): + self.platforms = [(b, re.compile(a, re.I)) for a, b in self.platforms] + self.browsers = [ + (b, re.compile(self._browser_version_re % a, re.I)) + for a, b in self.browsers + ] + + def __call__(self, user_agent): + for platform, regex in self.platforms: # noqa: B007 + match = regex.search(user_agent) + if match is not None: + break + else: + platform = None + for browser, regex in self.browsers: # noqa: B007 + match = regex.search(user_agent) + if match is not None: + version = match.group(1) + break + else: + browser = version = None + match = self._language_re.search(user_agent) + if match is not None: + language = match.group(1) or match.group(2) + else: + language = None + return platform, browser, version, language + + +class UserAgent(object): + """Represents a user agent. Pass it a WSGI environment or a user agent + string and you can inspect some of the details from the user agent + string via the attributes. The following attributes exist: + + .. attribute:: string + + the raw user agent string + + .. attribute:: platform + + the browser platform. The following platforms are currently + recognized: + + - `aix` + - `amiga` + - `android` + - `blackberry` + - `bsd` + - `chromeos` + - `dragonflybsd` + - `freebsd` + - `hpux` + - `ipad` + - `iphone` + - `irix` + - `linux` + - `macos` + - `netbsd` + - `openbsd` + - `sco` + - `solaris` + - `symbian` + - `wii` + - `windows` + + .. attribute:: browser + + the name of the browser. The following browsers are currently + recognized: + + - `aol` * + - `ask` * + - `baidu` * + - `bing` * + - `camino` + - `chrome` + - `edge` + - `firefox` + - `galeon` + - `google` * + - `kmeleon` + - `konqueror` + - `links` + - `lynx` + - `mozilla` + - `msie` + - `msn` + - `netscape` + - `opera` + - `safari` + - `seamonkey` + - `webkit` + - `yahoo` * + + (Browsers marked with a star (``*``) are crawlers.) + + .. attribute:: version + + the version of the browser + + .. attribute:: language + + the language of the browser + """ + + _parser = UserAgentParser() + + def __init__(self, environ_or_string): + if isinstance(environ_or_string, dict): + environ_or_string = environ_or_string.get("HTTP_USER_AGENT", "") + self.string = environ_or_string + self.platform, self.browser, self.version, self.language = self._parser( + environ_or_string + ) + + def to_header(self): + return self.string + + def __str__(self): + return self.string + + def __nonzero__(self): + return bool(self.browser) + + __bool__ = __nonzero__ + + def __repr__(self): + return "<%s %r/%s>" % (self.__class__.__name__, self.browser, self.version) diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 8059a0d802b52f76f9e46cc0cc37d49751ed8bbf..40c5664afcaa95978a10e3300dcc9e27ab2e7e39 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -35,7 +35,6 @@ import babel import babel.dates import passlib.utils import pytz -import werkzeug.utils from lxml import etree import odoo @@ -1239,13 +1238,27 @@ def ignore(*exc): except exc: pass -# Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9 -if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0'): - def html_escape(text): - return werkzeug.utils.escape(text, quote=True) -else: - def html_escape(text): - return werkzeug.utils.escape(text) +def html_escape(text): + """ Vendored from werkzeug.utils.escape which is deprecated in 2.0 + Replace special characters "&", "<", ">" and (") to HTML-safe sequences. + + There is a special handling for `None` which escapes to an empty string. + + :param s: the string to escape. + """ + if text is None: + return "" + elif hasattr(text, "__html__"): + return str(text.__html__()) + elif not isinstance(text, str): + text = str(text) + text = ( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + ) + return text def get_lang(env, lang_code=False): """ diff --git a/odoo/tools/safe_eval.py b/odoo/tools/safe_eval.py index ce47edb9d0f193a4b13022e8d478c618c0c97b9f..ca21cc7210afd3f6614b66f253f72adcee3fb3c3 100644 --- a/odoo/tools/safe_eval.py +++ b/odoo/tools/safe_eval.py @@ -89,6 +89,7 @@ _EXPR_OPCODES = _CONST_OPCODES.union(to_opcodes([ ])) - _BLACKLIST _SAFE_OPCODES = _EXPR_OPCODES.union(to_opcodes([ + 'GEN_START', # added in 3.10 'POP_BLOCK', 'POP_EXCEPT', # note: removed in 3.8 diff --git a/requirements.txt b/requirements.txt index b46ed6a3370611ac4f06d3cbf672406e91109f2d..5f0139ae7494f7520bb32f88aac7744334d0a079 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -Babel==2.6.0 +Babel==2.6.0; python_version <= '3.9' +Babel==2.9.1; python_version > '3.9' # (Jammy) 2.6.0 has issues with python 3.10 chardet==3.0.4 decorator==4.3.0 docutils==0.14 @@ -6,12 +7,14 @@ ebaysdk==2.1.5 freezegun==0.3.11; python_version < '3.8' freezegun==0.3.15; python_version >= '3.8' gevent==1.1.2 ; sys_platform != 'win32' and python_version < '3.7' -gevent==1.5.0 ; python_version == '3.7' -gevent==20.9.0 ; python_version >= '3.8' gevent==1.4.0 ; sys_platform == 'win32' and python_version < '3.7' +gevent==1.5.0 ; python_version == '3.7' +gevent==20.9.0 ; python_version > '3.7' and python_version <= '3.9' +gevent==21.8.0 ; python_version > '3.9' # (Jammy) greenlet==0.4.10 ; python_version < '3.7' greenlet==0.4.15 ; python_version == '3.7' -greenlet==0.4.17 ; python_version > '3.7' +greenlet==0.4.17 ; python_version > '3.7' and python_version <= '3.9' +greenlet==1.1.2 ; python_version > '3.9' # (Jammy) idna==2.6 Jinja2==2.10.1; python_version < '3.8' # bullseye version, focal patched 2.10 @@ -19,12 +22,14 @@ Jinja2==2.11.2; python_version >= '3.8' libsass==0.17.0 lxml==3.7.1 ; sys_platform != 'win32' and python_version < '3.7' lxml==4.3.2 ; sys_platform != 'win32' and python_version == '3.7' -lxml==4.6.1 ; sys_platform != 'win32' and python_version > '3.7' +# lxml 4.6.1 has incompatibility issues with python 3.10 +lxml==4.6.5 ; sys_platform != 'win32' and python_version > '3.7' # min version = 4.5.0 (Focal - with security backports) lxml ; sys_platform == 'win32' Mako==1.0.7 MarkupSafe==1.1.0 num2words==0.5.6 -ofxparse==0.19 +ofxparse==0.19; python_version <= '3.9' +ofxparse==0.21; python_version > '3.9' # (Jammy) ABC removed from collections in 3.10 but still used in ofxparse < 0.21 passlib==1.7.1 Pillow==5.4.1 ; python_version <= '3.7' and sys_platform != 'win32' Pillow==6.1.0 ; python_version <= '3.7' and sys_platform == 'win32' @@ -43,11 +48,14 @@ pyusb==1.0.2 qrcode==6.1 reportlab==3.5.13; python_version < '3.8' reportlab==3.5.55; python_version >= '3.8' -requests==2.21.0 +requests==2.21.0; python_version <= '3.9' +requests==2.25.1; python_version > '3.9' # (Jammy) versions < 2.25 aren't compatible w/ urllib3 1.26. Bullseye = 2.25.1. min version = 2.22.0 (Focal) +urllib3==1.26.5; python_version > '3.9' # (Jammy) indirect / min version = 1.25.8 (Focal with security backports) zeep==3.2.0 python-stdnum==1.8 vobject==0.9.6.1 -Werkzeug==0.16.1 +Werkzeug==0.16.1 ; python_version <= '3.9' +Werkzeug==2.0.2 ; python_version > '3.9' # (Jammy) XlsxWriter==1.1.2 xlwt==1.3.* xlrd==1.1.0; python_version < '3.8'