Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,223 @@
"""
Vendored copy of the werkzeug.utils.send_file function defined in
werkzeug2 which is packaged in Debian 12 "Bookworm" and Ubuntu 22.04
"Jammy". Odoo is compatible with werkzeug2 since saas-15.4.
This vendored copy is deprecated, only present to ensure backward
compatibility with older operating systems.
:copyright: 2007 Pallets
:license: BSD-3-Clause
"""
import io
import logging
import mimetypes
import os
import typing as t
import unicodedata
from datetime import datetime
from time import time
from zlib import adler32
from werkzeug.datastructures import Headers
from werkzeug.exceptions import RequestedRangeNotSatisfiable
from werkzeug.urls import url_quote
from werkzeug.wrappers import Response
from werkzeug.wsgi import wrap_file
_logger = logging.getLogger(__name__)
def send_file(
path_or_file: t.Union[os.PathLike, str, t.IO[bytes]],
environ: "WSGIEnvironment",
mimetype: t.Optional[str] = None,
as_attachment: bool = False,
download_name: t.Optional[str] = None,
conditional: bool = True,
etag: t.Union[bool, str] = True,
last_modified: t.Optional[t.Union[datetime, int, float]] = None,
max_age: t.Optional[
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
] = None,
use_x_sendfile: bool = False,
response_class: t.Optional[t.Type["Response"]] = None,
_root_path: t.Optional[t.Union[os.PathLike, str]] = None,
) -> "Response":
"""Send the contents of a file to the client.
The first argument can be a file path or a file-like object. Paths
are preferred in most cases because Werkzeug can manage the file and
get extra information from the path. Passing a file-like object
requires that the file is opened in binary mode, and is mostly
useful when building a file in memory with :class:`io.BytesIO`.
Never pass file paths provided by a user. The path is assumed to be
trusted, so a user could craft a path to access a file you didn't
intend.
If the WSGI server sets a ``file_wrapper`` in ``environ``, it is
used, otherwise Werkzeug's built-in wrapper is used. Alternatively,
if the HTTP server supports ``X-Sendfile``, ``use_x_sendfile=True``
will tell the server to send the given path, which is much more
efficient than reading it in Python.
:param path_or_file: The path to the file to send, relative to the
current working directory if a relative path is given.
Alternatively, a file-like object opened in binary mode. Make
sure the file pointer is seeked to the start of the data.
:param environ: The WSGI environ for the current request.
:param mimetype: The MIME type to send for the file. If not
provided, it will try to detect it from the file name.
:param as_attachment: Indicate to a browser that it should offer to
save the file instead of displaying it.
:param download_name: The default name browsers will use when saving
the file. Defaults to the passed file name.
:param conditional: Enable conditional and range responses based on
request headers. Requires passing a file path and ``environ``.
:param etag: Calculate an ETag for the file, which requires passing
a file path. Can also be a string to use instead.
:param last_modified: The last modified time to send for the file,
in seconds. If not provided, it will try to detect it from the
file path.
:param max_age: How long the client should cache the file, in
seconds. If set, ``Cache-Control`` will be ``public``, otherwise
it will be ``no-cache`` to prefer conditional caching.
:param use_x_sendfile: Set the ``X-Sendfile`` header to let the
server to efficiently send the file. Requires support from the
HTTP server. Requires passing a file path.
:param response_class: Build the response using this class. Defaults
to :class:`~werkzeug.wrappers.Response`.
:param _root_path: Do not use. For internal use only. Use
:func:`send_from_directory` to safely send files under a path.
"""
if response_class is None:
response_class = Response
path = None
file = None
size = None
mtime = None
headers = Headers()
if isinstance(path_or_file, (os.PathLike, str)) or hasattr(
path_or_file, "__fspath__"
):
# Flask will pass app.root_path, allowing its send_file wrapper
# to not have to deal with paths.
if _root_path is not None:
path = os.path.join(_root_path, path_or_file)
else:
path = os.path.abspath(path_or_file)
stat = os.stat(path)
size = stat.st_size
mtime = stat.st_mtime
else:
file = path_or_file
if download_name is None and path is not None:
download_name = os.path.basename(path)
if mimetype is None:
if download_name is None:
raise TypeError(
"Unable to detect the MIME type because a file name is"
" not available. Either set 'download_name', pass a"
" path instead of a file, or set 'mimetype'."
)
mimetype, encoding = mimetypes.guess_type(download_name)
if mimetype is None:
mimetype = "application/octet-stream"
# Don't send encoding for attachments, it causes browsers to
# save decompress tar.gz files.
if encoding is not None and not as_attachment:
headers.set("Content-Encoding", encoding)
if use_x_sendfile and path is not None:
headers["X-Accel-Charset"] = encoding
if download_name is not None:
try:
download_name.encode("ascii")
except UnicodeEncodeError:
simple = unicodedata.normalize("NFKD", download_name)
simple = simple.encode("ascii", "ignore").decode("ascii")
quoted = url_quote(download_name, safe="")
names = {"filename": simple, "filename*": f"UTF-8''{quoted}"}
else:
names = {"filename": download_name}
value = "attachment" if as_attachment else "inline"
headers.set("Content-Disposition", value, **names)
elif as_attachment:
raise TypeError(
"No name provided for attachment. Either set"
" 'download_name' or pass a path instead of a file."
)
if use_x_sendfile and path is not None:
headers["X-Sendfile"] = path
data = None
else:
if file is None:
file = open(path, "rb") # type: ignore
elif isinstance(file, io.BytesIO):
size = file.getbuffer().nbytes
elif isinstance(file, io.TextIOBase):
raise ValueError("Files must be opened in binary mode or use BytesIO.")
data = wrap_file(environ, file)
rv = response_class(
data, mimetype=mimetype, headers=headers, direct_passthrough=True
)
if size is not None:
rv.content_length = size
if last_modified is not None:
rv.last_modified = last_modified # type: ignore
elif mtime is not None:
rv.last_modified = mtime # type: ignore
rv.cache_control.no_cache = True
# Flask will pass app.get_send_file_max_age, allowing its send_file
# wrapper to not have to deal with paths.
if callable(max_age):
max_age = max_age(path)
if max_age is not None:
if max_age > 0:
rv.cache_control.no_cache = None
rv.cache_control.public = True
rv.cache_control.max_age = max_age
rv.expires = int(time() + max_age) # type: ignore
if isinstance(etag, str):
rv.set_etag(etag)
elif etag and path is not None:
check = adler32(path.encode("utf-8")) & 0xFFFFFFFF
rv.set_etag(f"{mtime}-{size}-{check}")
if conditional:
try:
rv = rv.make_conditional(environ, accept_ranges=True, complete_length=size)
except RequestedRangeNotSatisfiable:
if file is not None:
file.close()
raise
# Some x-sendfile implementations incorrectly ignore the 304
# status code and send the file anyway.
if rv.status_code == 304:
rv.headers.pop("x-sendfile", None)
return rv

View file

@ -0,0 +1,250 @@
# -*- 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 logging
import os
import re
import tempfile
from hashlib import sha1
from os import path, replace as rename
from odoo.tools.misc import pickle
from time import time
from werkzeug.datastructures import CallbackDict
_logger = logging.getLogger(__name__)
_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", "on_update")
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:
pickle.dump(dict(session), f, pickle.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:
_logger.debug('Could not load session from disk. Use empty session.', exc_info=True)
if self.renew_missing:
return self.new()
data = {}
else:
try:
try:
data = pickle.load(f, errors={})
except Exception:
_logger.debug('Could not load session data. Use empty session.', exc_info=True)
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

View file

@ -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)