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 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 55484609b57..90eeeaa165e 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 @@ -1266,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): @@ -1301,7 +1305,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/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/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) 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] 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 diff --git a/odoo/tools/_vendor/sessions.py b/odoo/tools/_vendor/sessions.py new file mode 100644 index 00000000000..c2c3a643cea --- /dev/null +++ b/odoo/tools/_vendor/sessions.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +r""" + Vendored copy of https://github.com/pallets/werkzeug/blob/2b2c4c3dd3cf7389e9f4aa06371b7332257c6289/src/werkzeug/contrib/sessions.py + + 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. + + 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 +""" +import os +import re +import tempfile +from hashlib import sha1 +from os import path +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}$") + + +def generate_key(salt=None): + if salt is None: + salt = repr(salt).encode("ascii") + return sha1(b"".join([salt, str(time()).encode("ascii"), os.urandom(30)])).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 + 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. + 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 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