From 92b007cb8956fbfd1eecd955d4946b37756fd726 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 16 Apr 2020 12:13:10 +0000 Subject: [PATCH 1/8] [IMP] core: update werkzeug dep to 0.16 allows having DeprecationWarnings pop up on runbot --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8f3ee1b5251..3157d26b992 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ requests==2.21.0 zeep==3.2.0 python-stdnum==1.8 vobject==0.9.6.1 -Werkzeug==0.14.1 +Werkzeug==0.16.1 XlsxWriter==1.1.2 xlwt==1.3.* xlrd==1.1.0 From 3f2a18b0f2d7a1f8da9ce8038278a2cc7b108c55 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 16 Apr 2020 11:38:32 +0000 Subject: [PATCH 2/8] [FIX] core: improve warnings configuration DeprecationWarning was configured as `once` which might be why I missed some previously: `once` means the *filter* will only signal once, meaning only one DeprecationWarning gets shown per run. Even if there are 5 *completely different* warnings getting generated by the codebase. `default` should be more suitable, I think I'd originally misunderstood it as "whatever the default is for this warning" but what it *actually* means is "print once for each (module, lineno)". An alternative could be `module` which would show the warning once per module but wouldn't trigger from different warnings in the same module. --- odoo/netsvc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo/netsvc.py b/odoo/netsvc.py index 9025532f32f..3e387352d0d 100644 --- a/odoo/netsvc.py +++ b/odoo/netsvc.py @@ -126,7 +126,7 @@ def init_logger(): logging.setLogRecordFactory(record_factory) # enable deprecation warnings (disabled by default) - warnings.filterwarnings('once', category=DeprecationWarning) + 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) From 39fb7f9d31ccc64091111c62b2dc34341f18a13d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 22 Apr 2020 09:09:36 +0000 Subject: [PATCH 3/8] [ADD] core: vendor werkzeug 0.16's sessions.py In Werkzeug 1.0, sessions support was moved out and into a separate package (secure-cookies). Issue is distros are already starting to update werkzeug to 1.0, without necessarily adding secure-cookies (e.g. arch, debian experimental). Plus secure-cookies has some changes e.g. different filename & al. So just vendor the "continuity" version which is the last werkzeug before removal. This commit copies the original file as-is so we can track eventual changes if necessary. --- odoo/tools/_vendor/sessions.py | 389 +++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 odoo/tools/_vendor/sessions.py diff --git a/odoo/tools/_vendor/sessions.py b/odoo/tools/_vendor/sessions.py new file mode 100644 index 00000000000..53567a1ccdd --- /dev/null +++ b/odoo/tools/_vendor/sessions.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- +r""" + werkzeug.contrib.sessions + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + This module contains some helper classes that help one to add session + support to a python WSGI application. For full client-side session + storage see :mod:`~werkzeug.contrib.securecookie` which implements a + secure, client-side session storage. + + + Application Integration + ======================= + + :: + + from werkzeug.contrib.sessions import SessionMiddleware, \ + FilesystemSessionStore + + app = SessionMiddleware(app, FilesystemSessionStore()) + + The current session will then appear in the WSGI environment as + `werkzeug.session`. However it's recommended to not use the middleware + but the stores directly in the application. However for very simple + scripts a middleware for sessions could be sufficient. + + This module does not implement methods or ways to check if a session is + expired. That should be done by a cronjob and storage specific. For + example to prune unused filesystem sessions one could check the modified + time of the files. If sessions are stored in the database the new() + method should add an expiration timestamp for the session. + + For better flexibility it's recommended to not use the middleware but the + store and session object directly in the application dispatching:: + + session_store = FilesystemSessionStore() + + def application(environ, start_response): + request = Request(environ) + sid = request.cookies.get('cookie_name') + if sid is None: + request.session = session_store.new() + else: + request.session = session_store.get(sid) + response = get_the_response_object(request) + if request.session.should_save: + session_store.save(request.session) + response.set_cookie('cookie_name', request.session.sid) + return response(environ, start_response) + + :copyright: 2007 Pallets + :license: BSD-3-Clause +""" +import os +import re +import tempfile +import warnings +from hashlib import sha1 +from os import path +from pickle import dump +from pickle import HIGHEST_PROTOCOL +from pickle import load +from random import random +from time import time + +from .._compat import PY2 +from .._compat import text_type +from ..datastructures import CallbackDict +from ..filesystem import get_filesystem_encoding +from ..http import dump_cookie +from ..http import parse_cookie +from ..posixemulation import rename +from ..wsgi import ClosingIterator + +warnings.warn( + "'werkzeug.contrib.sessions' is deprecated as of version 0.15 and" + " will be removed in version 1.0. It has moved to" + " https://github.com/pallets/secure-cookie.", + DeprecationWarning, + stacklevel=2, +) + +_sha1_re = re.compile(r"^[a-f0-9]{40}$") + + +def _urandom(): + if hasattr(os, "urandom"): + return os.urandom(30) + return text_type(random()).encode("ascii") + + +def generate_key(salt=None): + if salt is None: + salt = repr(salt).encode("ascii") + return sha1(b"".join([salt, str(time()).encode("ascii"), _urandom()])).hexdigest() + + +class ModificationTrackingDict(CallbackDict): + __slots__ = ("modified",) + + def __init__(self, *args, **kwargs): + def on_update(self): + self.modified = True + + self.modified = False + CallbackDict.__init__(self, on_update=on_update) + dict.update(self, *args, **kwargs) + + def copy(self): + """Create a flat copy of the dict.""" + missing = object() + result = object.__new__(self.__class__) + for name in self.__slots__: + val = getattr(self, name, missing) + if val is not missing: + setattr(result, name, val) + return result + + def __copy__(self): + return self.copy() + + +class Session(ModificationTrackingDict): + """Subclass of a dict that keeps track of direct object changes. Changes + in mutable structures are not tracked, for those you have to set + `modified` to `True` by hand. + """ + + __slots__ = ModificationTrackingDict.__slots__ + ("sid", "new") + + def __init__(self, data, sid, new=False): + ModificationTrackingDict.__init__(self, data) + self.sid = sid + self.new = new + + def __repr__(self): + return "<%s %s%s>" % ( + self.__class__.__name__, + dict.__repr__(self), + "*" if self.should_save else "", + ) + + @property + def should_save(self): + """True if the session should be saved. + + .. versionchanged:: 0.6 + By default the session is now only saved if the session is + modified, not if it is new like it was before. + """ + return self.modified + + +class SessionStore(object): + """Baseclass for all session stores. The Werkzeug contrib module does not + implement any useful stores besides the filesystem store, application + developers are encouraged to create their own stores. + + :param session_class: The session class to use. Defaults to + :class:`Session`. + """ + + def __init__(self, session_class=None): + if session_class is None: + session_class = Session + self.session_class = session_class + + def is_valid_key(self, key): + """Check if a key has the correct format.""" + return _sha1_re.match(key) is not None + + def generate_key(self, salt=None): + """Simple function that generates a new session key.""" + return generate_key(salt) + + def new(self): + """Generate a new session.""" + return self.session_class({}, self.generate_key(), True) + + def save(self, session): + """Save a session.""" + + def save_if_modified(self, session): + """Save if a session class wants an update.""" + if session.should_save: + self.save(session) + + def delete(self, session): + """Delete a session.""" + + def get(self, sid): + """Get a session for this sid or a new session object. This method + has to check if the session key is valid and create a new session if + that wasn't the case. + """ + return self.session_class({}, sid, True) + + +#: used for temporary files by the filesystem session store +_fs_transaction_suffix = ".__wz_sess" + + +class FilesystemSessionStore(SessionStore): + """Simple example session store that saves sessions on the filesystem. + This store works best on POSIX systems and Windows Vista / Windows + Server 2008 and newer. + + .. versionchanged:: 0.6 + `renew_missing` was added. Previously this was considered `True`, + now the default changed to `False` and it can be explicitly + deactivated. + + :param path: the path to the folder used for storing the sessions. + If not provided the default temporary directory is used. + :param filename_template: a string template used to give the session + a filename. ``%s`` is replaced with the + session id. + :param session_class: The session class to use. Defaults to + :class:`Session`. + :param renew_missing: set to `True` if you want the store to + give the user a new sid if the session was + not yet saved. + """ + + def __init__( + self, + path=None, + filename_template="werkzeug_%s.sess", + session_class=None, + renew_missing=False, + mode=0o644, + ): + SessionStore.__init__(self, session_class) + if path is None: + path = tempfile.gettempdir() + self.path = path + if isinstance(filename_template, text_type) and PY2: + filename_template = filename_template.encode(get_filesystem_encoding()) + assert not filename_template.endswith(_fs_transaction_suffix), ( + "filename templates may not end with %s" % _fs_transaction_suffix + ) + self.filename_template = filename_template + self.renew_missing = renew_missing + self.mode = mode + + def get_session_filename(self, sid): + # out of the box, this should be a strict ASCII subset but + # you might reconfigure the session object to have a more + # arbitrary string. + if isinstance(sid, text_type) and PY2: + sid = sid.encode(get_filesystem_encoding()) + return path.join(self.path, self.filename_template % sid) + + def save(self, session): + fn = self.get_session_filename(session.sid) + fd, tmp = tempfile.mkstemp(suffix=_fs_transaction_suffix, dir=self.path) + f = os.fdopen(fd, "wb") + try: + dump(dict(session), f, HIGHEST_PROTOCOL) + finally: + f.close() + try: + rename(tmp, fn) + os.chmod(fn, self.mode) + except (IOError, OSError): + pass + + def delete(self, session): + fn = self.get_session_filename(session.sid) + try: + os.unlink(fn) + except OSError: + pass + + def get(self, sid): + if not self.is_valid_key(sid): + return self.new() + try: + f = open(self.get_session_filename(sid), "rb") + except IOError: + if self.renew_missing: + return self.new() + data = {} + else: + try: + try: + data = load(f) + except Exception: + data = {} + finally: + f.close() + return self.session_class(data, sid, False) + + def list(self): + """Lists all sessions in the store. + + .. versionadded:: 0.6 + """ + before, after = self.filename_template.split("%s", 1) + filename_re = re.compile( + r"%s(.{5,})%s$" % (re.escape(before), re.escape(after)) + ) + result = [] + for filename in os.listdir(self.path): + #: this is a session that is still being saved. + if filename.endswith(_fs_transaction_suffix): + continue + match = filename_re.match(filename) + if match is not None: + result.append(match.group(1)) + return result + + +class SessionMiddleware(object): + """A simple middleware that puts the session object of a store provided + into the WSGI environ. It automatically sets cookies and restores + sessions. + + However a middleware is not the preferred solution because it won't be as + fast as sessions managed by the application itself and will put a key into + the WSGI environment only relevant for the application which is against + the concept of WSGI. + + The cookie parameters are the same as for the :func:`~dump_cookie` + function just prefixed with ``cookie_``. Additionally `max_age` is + called `cookie_age` and not `cookie_max_age` because of backwards + compatibility. + """ + + def __init__( + self, + app, + store, + cookie_name="session_id", + cookie_age=None, + cookie_expires=None, + cookie_path="/", + cookie_domain=None, + cookie_secure=None, + cookie_httponly=False, + cookie_samesite="Lax", + environ_key="werkzeug.session", + ): + self.app = app + self.store = store + self.cookie_name = cookie_name + self.cookie_age = cookie_age + self.cookie_expires = cookie_expires + self.cookie_path = cookie_path + self.cookie_domain = cookie_domain + self.cookie_secure = cookie_secure + self.cookie_httponly = cookie_httponly + self.cookie_samesite = cookie_samesite + self.environ_key = environ_key + + def __call__(self, environ, start_response): + cookie = parse_cookie(environ.get("HTTP_COOKIE", "")) + sid = cookie.get(self.cookie_name, None) + if sid is None: + session = self.store.new() + else: + session = self.store.get(sid) + environ[self.environ_key] = session + + def injecting_start_response(status, headers, exc_info=None): + if session.should_save: + self.store.save(session) + headers.append( + ( + "Set-Cookie", + dump_cookie( + self.cookie_name, + session.sid, + self.cookie_age, + self.cookie_expires, + self.cookie_path, + self.cookie_domain, + self.cookie_secure, + self.cookie_httponly, + samesite=self.cookie_samesite, + ), + ) + ) + return start_response(status, headers, exc_info) + + return ClosingIterator( + self.app(environ, injecting_start_response), + lambda: self.store.save_if_modified(session), + ) From 9d4ee88263832b8444cfd49582eab3ceab3d3281 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 22 Apr 2020 09:13:58 +0000 Subject: [PATCH 4/8] [FIX] core: vendored sessions.py so it actually works * remove SessionMiddleware we don't need as it's responsible for most imports * fix relative imports to absolute imports from werkzeug * remove py2/py3 compatibility imports, shims and conditions as we're P3 only * remove deprecation warning (duh) --- odoo/tools/_vendor/sessions.py | 164 +++------------------------------ 1 file changed, 12 insertions(+), 152 deletions(-) diff --git a/odoo/tools/_vendor/sessions.py b/odoo/tools/_vendor/sessions.py index 53567a1ccdd..c2c3a643cea 100644 --- a/odoo/tools/_vendor/sessions.py +++ b/odoo/tools/_vendor/sessions.py @@ -1,52 +1,16 @@ # -*- coding: utf-8 -*- r""" - werkzeug.contrib.sessions - ~~~~~~~~~~~~~~~~~~~~~~~~~ + Vendored copy of https://github.com/pallets/werkzeug/blob/2b2c4c3dd3cf7389e9f4aa06371b7332257c6289/src/werkzeug/contrib/sessions.py - This module contains some helper classes that help one to add session - support to a python WSGI application. For full client-side session - storage see :mod:`~werkzeug.contrib.securecookie` which implements a - secure, client-side session storage. + werkzeug.contrib was removed from werkzeug 1.0. sessions (and secure + cookies) were moved to the secure-cookies package. Problem is distros + are starting to update werkzeug to 1.0 without having secure-cookies + (e.g. Arch has done so, Debian has updated python-werkzeug in + "experimental"), which will be problematic once that starts trickling + down onto more stable distros and people start deploying that. - - Application Integration - ======================= - - :: - - from werkzeug.contrib.sessions import SessionMiddleware, \ - FilesystemSessionStore - - app = SessionMiddleware(app, FilesystemSessionStore()) - - The current session will then appear in the WSGI environment as - `werkzeug.session`. However it's recommended to not use the middleware - but the stores directly in the application. However for very simple - scripts a middleware for sessions could be sufficient. - - This module does not implement methods or ways to check if a session is - expired. That should be done by a cronjob and storage specific. For - example to prune unused filesystem sessions one could check the modified - time of the files. If sessions are stored in the database the new() - method should add an expiration timestamp for the session. - - For better flexibility it's recommended to not use the middleware but the - store and session object directly in the application dispatching:: - - session_store = FilesystemSessionStore() - - def application(environ, start_response): - request = Request(environ) - sid = request.cookies.get('cookie_name') - if sid is None: - request.session = session_store.new() - else: - request.session = session_store.get(sid) - response = get_the_response_object(request) - if request.session.should_save: - session_store.save(request.session) - response.set_cookie('cookie_name', request.session.sid) - return response(environ, start_response) + Edited some to fix imports and remove some compatibility things + (mostly PY2) and the unnecessary (to us) SessionMiddleware :copyright: 2007 Pallets :license: BSD-3-Clause @@ -54,45 +18,23 @@ r""" import os import re import tempfile -import warnings from hashlib import sha1 from os import path from pickle import dump from pickle import HIGHEST_PROTOCOL from pickle import load -from random import random from time import time -from .._compat import PY2 -from .._compat import text_type -from ..datastructures import CallbackDict -from ..filesystem import get_filesystem_encoding -from ..http import dump_cookie -from ..http import parse_cookie -from ..posixemulation import rename -from ..wsgi import ClosingIterator - -warnings.warn( - "'werkzeug.contrib.sessions' is deprecated as of version 0.15 and" - " will be removed in version 1.0. It has moved to" - " https://github.com/pallets/secure-cookie.", - DeprecationWarning, - stacklevel=2, -) +from werkzeug.datastructures import CallbackDict +from werkzeug.posixemulation import rename _sha1_re = re.compile(r"^[a-f0-9]{40}$") -def _urandom(): - if hasattr(os, "urandom"): - return os.urandom(30) - return text_type(random()).encode("ascii") - - def generate_key(salt=None): if salt is None: salt = repr(salt).encode("ascii") - return sha1(b"".join([salt, str(time()).encode("ascii"), _urandom()])).hexdigest() + return sha1(b"".join([salt, str(time()).encode("ascii"), os.urandom(30)])).hexdigest() class ModificationTrackingDict(CallbackDict): @@ -234,8 +176,6 @@ class FilesystemSessionStore(SessionStore): if path is None: path = tempfile.gettempdir() self.path = path - if isinstance(filename_template, text_type) and PY2: - filename_template = filename_template.encode(get_filesystem_encoding()) assert not filename_template.endswith(_fs_transaction_suffix), ( "filename templates may not end with %s" % _fs_transaction_suffix ) @@ -247,8 +187,6 @@ class FilesystemSessionStore(SessionStore): # out of the box, this should be a strict ASCII subset but # you might reconfigure the session object to have a more # arbitrary string. - if isinstance(sid, text_type) and PY2: - sid = sid.encode(get_filesystem_encoding()) return path.join(self.path, self.filename_template % sid) def save(self, session): @@ -309,81 +247,3 @@ class FilesystemSessionStore(SessionStore): if match is not None: result.append(match.group(1)) return result - - -class SessionMiddleware(object): - """A simple middleware that puts the session object of a store provided - into the WSGI environ. It automatically sets cookies and restores - sessions. - - However a middleware is not the preferred solution because it won't be as - fast as sessions managed by the application itself and will put a key into - the WSGI environment only relevant for the application which is against - the concept of WSGI. - - The cookie parameters are the same as for the :func:`~dump_cookie` - function just prefixed with ``cookie_``. Additionally `max_age` is - called `cookie_age` and not `cookie_max_age` because of backwards - compatibility. - """ - - def __init__( - self, - app, - store, - cookie_name="session_id", - cookie_age=None, - cookie_expires=None, - cookie_path="/", - cookie_domain=None, - cookie_secure=None, - cookie_httponly=False, - cookie_samesite="Lax", - environ_key="werkzeug.session", - ): - self.app = app - self.store = store - self.cookie_name = cookie_name - self.cookie_age = cookie_age - self.cookie_expires = cookie_expires - self.cookie_path = cookie_path - self.cookie_domain = cookie_domain - self.cookie_secure = cookie_secure - self.cookie_httponly = cookie_httponly - self.cookie_samesite = cookie_samesite - self.environ_key = environ_key - - def __call__(self, environ, start_response): - cookie = parse_cookie(environ.get("HTTP_COOKIE", "")) - sid = cookie.get(self.cookie_name, None) - if sid is None: - session = self.store.new() - else: - session = self.store.get(sid) - environ[self.environ_key] = session - - def injecting_start_response(status, headers, exc_info=None): - if session.should_save: - self.store.save(session) - headers.append( - ( - "Set-Cookie", - dump_cookie( - self.cookie_name, - session.sid, - self.cookie_age, - self.cookie_expires, - self.cookie_path, - self.cookie_domain, - self.cookie_secure, - self.cookie_httponly, - samesite=self.cookie_samesite, - ), - ) - ) - return start_response(status, headers, exc_info) - - return ClosingIterator( - self.app(environ, injecting_start_response), - lambda: self.store.save_if_modified(session), - ) From c58c61883389d10b58837189c03642dc8becec39 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 21 Feb 2020 12:32:38 +0000 Subject: [PATCH 5/8] [FIX] core: modules moved or removed in Werkzeug 1.0 Those were deprecations implemented in 0.15 * all middlewares have been moved from `werkzeug.wsgi` to `werkzeug.middleware`, including the `SharedDataMiddleware` we use * ProxyFix was moved to werkzeug.middleware.proxy_fix, this had already been fixed but I forgot the import * sessions support was moved to a separate package (`pallets/secure-cookies`), however while distros are starting to update werkzeug to 1.0 (e.g. done on Arch, and in Debian Experimental) they're not bundling secure-cookies so using a vendored version seems like the least bad thing we can do, even more so as conditional dependencies are not really a thing (e.g. even with just pip we can't depend on secure-cookie iff werkzeug >= 1.0) --- odoo/http.py | 14 ++++++++------ odoo/service/wsgi_server.py | 1 - 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/odoo/http.py b/odoo/http.py index 55484609b57..b4729c20917 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -30,15 +30,17 @@ from datetime import datetime, date import passlib.utils import psycopg2 import json -import werkzeug.contrib.sessions import werkzeug.datastructures import werkzeug.exceptions import werkzeug.local import werkzeug.routing import werkzeug.wrappers -import werkzeug.wsgi from werkzeug import urls from werkzeug.wsgi import wrap_file +try: + from werkzeug.middleware.shared_data import SharedDataMiddleware +except ImportError: + from werkzeug.wsgi import SharedDataMiddleware try: import psutil @@ -53,7 +55,7 @@ from .sql_db import flush_env from .tools.func import lazy_property from .tools import ustr, consteq, frozendict, pycompat, unique, date_utils from .tools.mimetypes import guess_mimetype - +from .tools._vendor import sessions from .modules.module import module_manifest _logger = logging.getLogger(__name__) @@ -957,7 +959,7 @@ class AuthenticationError(Exception): class SessionExpiredException(Exception): pass -class OpenERPSession(werkzeug.contrib.sessions.Session): +class OpenERPSession(sessions.Session): def __init__(self, *args, **kwargs): self.inited = False self.modified = False @@ -1258,7 +1260,7 @@ class Root(object): # Setup http sessions path = odoo.tools.config.session_dir _logger.debug('HTTP sessions stored in: %s', path) - return werkzeug.contrib.sessions.FilesystemSessionStore( + return sessions.FilesystemSessionStore( path, session_class=OpenERPSession, renew_missing=True) @lazy_property @@ -1301,7 +1303,7 @@ class Root(object): if statics: _logger.info("HTTP Configuring static files") - app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics, cache_timeout=STATIC_CACHE) + app = SharedDataMiddleware(self.dispatch, statics, cache_timeout=STATIC_CACHE) self.dispatch = DisableCacheMiddleware(app) def setup_session(self, httprequest): diff --git a/odoo/service/wsgi_server.py b/odoo/service/wsgi_server.py index c2712203804..7072b4be7ac 100644 --- a/odoo/service/wsgi_server.py +++ b/odoo/service/wsgi_server.py @@ -17,7 +17,6 @@ from xmlrpc import client as xmlrpclib import werkzeug.exceptions import werkzeug.wrappers import werkzeug.serving -import werkzeug.contrib.fixers import odoo from odoo.tools import config From d9c685a75730a55e184ca32b75d82722d3f9091f Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 1 Apr 2020 08:29:29 +0000 Subject: [PATCH 6/8] [FIX] odoo_referral: werkzeug.url_encode removed from werkzeug 1.0 --- addons/odoo_referral/models/res_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/odoo_referral/models/res_users.py b/addons/odoo_referral/models/res_users.py index 114095ec0dc..522ba458806 100644 --- a/addons/odoo_referral/models/res_users.py +++ b/addons/odoo_referral/models/res_users.py @@ -8,7 +8,7 @@ import hmac import threading from hashlib import sha1 -from werkzeug import url_encode +from werkzeug.urls import url_encode from odoo import api, models, tools From 9a93d3db11d0cf7e5e10d4d3578aff73d64a4f61 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 1 Apr 2020 08:30:29 +0000 Subject: [PATCH 7/8] [FIX] *: collection. is deprecated --- odoo/modules/module.py | 4 ++-- odoo/osv/expression.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/odoo/modules/module.py b/odoo/modules/module.py index 88d96949b74..959019d56e8 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import ast -import collections +import collections.abc import importlib import inspect import itertools @@ -260,7 +260,7 @@ def load_information_from_description_file(module, mod_path=None): # auto_install: [] to always auto_install a module regardless of its # dependencies auto_install = info.get('auto_install', info.get('active', False)) - if isinstance(auto_install, collections.Iterable): + if isinstance(auto_install, collections.abc.Iterable): info['auto_install'] = set(auto_install) non_dependencies = info['auto_install'].difference(info['depends']) assert not non_dependencies,\ diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index ce1410b9225..12c2f2df5d7 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -113,7 +113,7 @@ Finally, to instruct OpenERP to really use the unaccent function, you have to start the server specifying the ``--unaccent`` flag. """ -import collections +import collections.abc import logging import traceback @@ -958,7 +958,7 @@ class expression(object): op2 = (TERM_OPERATORS_NEGATION[operator] if operator in NEGATIVE_TERM_OPERATORS else operator) ids2 = [x[0] for x in comodel.name_search(right, domain or [], op2, limit=None)] - elif isinstance(right, collections.Iterable): + elif isinstance(right, collections.abc.Iterable): ids2 = right else: ids2 = [right] @@ -1018,7 +1018,7 @@ class expression(object): op2 = (TERM_OPERATORS_NEGATION[operator] if operator in NEGATIVE_TERM_OPERATORS else operator) ids2 = [x[0] for x in comodel.name_search(right, domain or [], op2, limit=None)] - elif isinstance(right, collections.Iterable): + elif isinstance(right, collections.abc.Iterable): ids2 = right else: ids2 = [right] From 34f086607841f59d03318675b646fca3aede2fa9 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 1 Apr 2020 08:23:54 +0000 Subject: [PATCH 8/8] [FIX] core: disable werkzeug's `merge_slashes` Werkzeug 1.0 adds a `merge_slashes` feature which calls rule.build() *during match*[0] (= during dispatch). However at this point our environment still contains an invalid / placeholder UID, so `display_name` / `name_get()` fails dramatically. One possibility would be to update the slugifying to not access the record (outside of the id) if the environment is not "proper", an other alternative is to just disable the feature since it seems to not have been necessary so far. The latter seems less hacky so do that for now, we can always swap the solution later if we need to. [0] https://github.com/pallets/werkzeug/blame/048cdfd9b969c0c3a133d7ff43b8ad1ad6a673ec/src/werkzeug/routing.py#L904 --- odoo/addons/base/models/ir_http.py | 4 +++- odoo/http.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index ba771a95753..aa4b7e862c6 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -269,7 +269,9 @@ class IrHttp(models.AbstractModel): for url, endpoint, routing in cls._generate_routing_rules(mods, converters=cls._get_converters()): xtra_keys = 'defaults subdomain build_only strict_slashes redirect_to alias host'.split() kw = {k: routing[k] for k in xtra_keys if k in routing} - routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods'], **kw)) + rule = werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods'], **kw) + rule.merge_slashes = False + routing_map.add(rule) cls._routing_map[key] = routing_map return cls._routing_map[key] diff --git a/odoo/http.py b/odoo/http.py index b4729c20917..90eeeaa165e 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -1268,7 +1268,9 @@ class Root(object): _logger.info("Generating nondb routing") routing_map = werkzeug.routing.Map(strict_slashes=False, converters=None) for url, endpoint, routing in odoo.http._generate_routing_rules([''] + odoo.conf.server_wide_modules, True): - routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods'])) + rule = werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']) + rule.merge_slashes = False + routing_map.add(rule) return routing_map def __call__(self, environ, start_response):