Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package Radicale for openSUSE:Factory checked in at 2022-04-20 16:55:13 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/Radicale (Old) and /work/SRC/openSUSE:Factory/.Radicale.new.1941 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "Radicale" Wed Apr 20 16:55:13 2022 rev:6 rq:970767 version:3.1.6 Changes: -------- --- /work/SRC/openSUSE:Factory/Radicale/Radicale.changes 2022-02-18 23:04:00.829407824 +0100 +++ /work/SRC/openSUSE:Factory/.Radicale.new.1941/Radicale.changes 2022-04-20 16:55:18.286509541 +0200 @@ -1,0 +2,8 @@ +Tue Apr 19 09:30:05 UTC 2022 - Paolo Stivanin <[email protected]> + +- Update to 3.1.6: + * Ignore 'Not a directory' error for optional config paths + * Fix upload of whole address book/calendar with UIDs that collide on + case-insensitive filesystem + +------------------------------------------------------------------- Old: ---- Radicale-3.1.5.tar.gz New: ---- Radicale-3.1.6.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ Radicale.spec ++++++ --- /var/tmp/diff_new_pack.IbVO6y/_old 2022-04-20 16:55:19.222510415 +0200 +++ /var/tmp/diff_new_pack.IbVO6y/_new 2022-04-20 16:55:19.226510419 +0200 @@ -26,7 +26,7 @@ %define vo_min_ver 0.9.6 %define du_min_ver 2.7.3 Name: Radicale -Version: 3.1.5 +Version: 3.1.6 Release: 0 Summary: A CalDAV calendar and CardDav contact server License: GPL-3.0-or-later ++++++ Radicale-3.1.5.tar.gz -> Radicale-3.1.6.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/CHANGELOG.md new/Radicale-3.1.6/CHANGELOG.md --- old/Radicale-3.1.5/CHANGELOG.md 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/CHANGELOG.md 2022-04-18 23:10:01.000000000 +0200 @@ -1,5 +1,13 @@ # Changelog +## 3.1.6 + +* Ignore `Not a directory` error for optional config paths +* Fix upload of whole address book/calendar with UIDs that collide on + case-insensitive filesystem +* Remove runtime dependency on setuptools for Python>=3.9 +* Windows: Block ADS paths + ## 3.1.5 * Ignore configuration file if access is denied diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/__init__.py new/Radicale-3.1.6/radicale/__init__.py --- old/Radicale-3.1.5/radicale/__init__.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/__init__.py 2022-04-18 23:10:01.000000000 +0200 @@ -29,13 +29,11 @@ import threading from typing import Iterable, Optional, cast -import pkg_resources - -from radicale import config, log, types +from radicale import config, log, types, utils from radicale.app import Application from radicale.log import logger -VERSION: str = pkg_resources.get_distribution("radicale").version +VERSION: str = utils.package_version("radicale") _application_instance: Optional[Application] = None _application_config_path: Optional[str] = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/config.py new/Radicale-3.1.6/radicale/config.py --- old/Radicale-3.1.5/radicale/config.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/config.py 2022-04-18 23:10:01.000000000 +0200 @@ -283,8 +283,8 @@ config = {s: {o: parser[s][o] for o in parser.options(s)} for s in parser.sections()} except Exception as e: - if not (ignore_if_missing and - isinstance(e, (FileNotFoundError, PermissionError))): + if not (ignore_if_missing and isinstance(e, ( + FileNotFoundError, NotADirectoryError, PermissionError))): raise RuntimeError("Failed to load %s: %s" % (config_source, e) ) from e config = Configuration.SOURCE_MISSING diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/httputils.py new/Radicale-3.1.6/radicale/httputils.py --- old/Radicale-3.1.5/radicale/httputils.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/httputils.py 2022-04-18 23:10:01.000000000 +0200 @@ -24,13 +24,25 @@ import contextlib import os +import pathlib +import sys import time from http import client -from typing import List, Mapping, cast +from typing import List, Mapping, Union, cast from radicale import config, pathutils, types from radicale.log import logger +if sys.version_info < (3, 9): + import pkg_resources + + _TRAVERSABLE_LIKE_TYPE = pathlib.Path +else: + import importlib.abc + from importlib import resources + + _TRAVERSABLE_LIKE_TYPE = Union[importlib.abc.Traversable, pathlib.Path] + NOT_ALLOWED: types.WSGIResponse = ( client.FORBIDDEN, (("Content-Type", "text/plain"),), "Access to the requested resource forbidden.") @@ -140,36 +152,63 @@ "Redirected to %s" % location) -def serve_folder(folder: str, base_prefix: str, path: str, - path_prefix: str = "/.web", index_file: str = "index.html", - mimetypes: Mapping[str, str] = MIMETYPES, - fallback_mimetype: str = FALLBACK_MIMETYPE, - ) -> types.WSGIResponse: +def _serve_traversable( + traversable: _TRAVERSABLE_LIKE_TYPE, base_prefix: str, path: str, + path_prefix: str, index_file: str, mimetypes: Mapping[str, str], + fallback_mimetype: str) -> types.WSGIResponse: if path != path_prefix and not path.startswith(path_prefix): raise ValueError("path must start with path_prefix: %r --> %r" % (path_prefix, path)) assert pathutils.sanitize_path(path) == path - try: - filesystem_path = pathutils.path_to_filesystem( - folder, path[len(path_prefix):].strip("/")) - except ValueError as e: - logger.debug("Web content with unsafe path %r requested: %s", - path, e, exc_info=True) - return NOT_FOUND - if os.path.isdir(filesystem_path) and not path.endswith("/"): - return redirect(base_prefix + path + "/") - if os.path.isdir(filesystem_path) and index_file: - filesystem_path = os.path.join(filesystem_path, index_file) - if not os.path.isfile(filesystem_path): + parts_path = path[len(path_prefix):].strip('/') + parts = parts_path.split("/") if parts_path else [] + for part in parts: + if not pathutils.is_safe_filesystem_path_component(part): + logger.debug("Web content with unsafe path %r requested", path) + return NOT_FOUND + if (not traversable.is_dir() or + all(part != entry.name for entry in traversable.iterdir())): + return NOT_FOUND + traversable = traversable.joinpath(part) + if traversable.is_dir(): + if not path.endswith("/"): + return redirect(base_prefix + path + "/") + if not index_file: + return NOT_FOUND + traversable = traversable.joinpath(index_file) + if not traversable.is_file(): return NOT_FOUND content_type = MIMETYPES.get( - os.path.splitext(filesystem_path)[1].lower(), FALLBACK_MIMETYPE) - with open(filesystem_path, "rb") as f: - answer = f.read() - last_modified = time.strftime( + os.path.splitext(traversable.name)[1].lower(), FALLBACK_MIMETYPE) + headers = {"Content-Type": content_type} + if isinstance(traversable, pathlib.Path): + headers["Last-Modified"] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", - time.gmtime(os.fstat(f.fileno()).st_mtime)) - headers = { - "Content-Type": content_type, - "Last-Modified": last_modified} + time.gmtime(traversable.stat().st_mtime)) + answer = traversable.read_bytes() return client.OK, headers, answer + + +def serve_resource( + package: str, resource: str, base_prefix: str, path: str, + path_prefix: str = "/.web", index_file: str = "index.html", + mimetypes: Mapping[str, str] = MIMETYPES, + fallback_mimetype: str = FALLBACK_MIMETYPE) -> types.WSGIResponse: + if sys.version_info < (3, 9): + traversable = pathlib.Path( + pkg_resources.resource_filename(package, resource)) + else: + traversable = resources.files(package).joinpath(resource) + return _serve_traversable(traversable, base_prefix, path, path_prefix, + index_file, mimetypes, fallback_mimetype) + + +def serve_folder( + folder: str, base_prefix: str, path: str, + path_prefix: str = "/.web", index_file: str = "index.html", + mimetypes: Mapping[str, str] = MIMETYPES, + fallback_mimetype: str = FALLBACK_MIMETYPE) -> types.WSGIResponse: + # deprecated: use `serve_resource` instead + traversable = pathlib.Path(folder) + return _serve_traversable(traversable, base_prefix, path, path_prefix, + index_file, mimetypes, fallback_mimetype) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/pathutils.py new/Radicale-3.1.6/radicale/pathutils.py --- old/Radicale-3.1.5/radicale/pathutils.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/pathutils.py 2022-04-18 23:10:01.000000000 +0200 @@ -257,6 +257,7 @@ """ return ( bool(path) and not os.path.splitdrive(path)[0] and + (sys.platform != "win32" or ":" not in path) and # Block NTFS-ADS not os.path.split(path)[0] and path not in (os.curdir, os.pardir) and not path.startswith(".") and not path.endswith("~") and is_safe_path_component(path)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/storage/__init__.py new/Radicale-3.1.6/radicale/storage/__init__.py --- old/Radicale-3.1.5/radicale/storage/__init__.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/storage/__init__.py 2022-04-18 23:10:01.000000000 +0200 @@ -29,7 +29,6 @@ from typing import (Iterable, Iterator, Mapping, Optional, Sequence, Set, Tuple, Union, overload) -import pkg_resources import vobject from radicale import config @@ -41,7 +40,7 @@ CACHE_DEPS: Sequence[str] = ("radicale", "vobject", "python-dateutil",) CACHE_VERSION: bytes = "".join( - "%s=%s;" % (pkg, pkg_resources.get_distribution(pkg).version) + "%s=%s;" % (pkg, utils.package_version(pkg)) for pkg in CACHE_DEPS).encode() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/storage/multifilesystem/upload.py new/Radicale-3.1.6/radicale/storage/multifilesystem/upload.py --- old/Radicale-3.1.5/radicale/storage/multifilesystem/upload.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/storage/multifilesystem/upload.py 2022-04-18 23:10:01.000000000 +0200 @@ -20,7 +20,7 @@ import os import pickle import sys -from typing import Iterable, Set, TextIO, cast +from typing import Iterable, Iterator, TextIO, cast import radicale.item as radicale_item from radicale import pathutils @@ -59,16 +59,23 @@ def _upload_all_nonatomic(self, items: Iterable[radicale_item.Item], suffix: str = "") -> None: - """Upload a new set of items. + """Upload a new set of items non-atomic""" + def is_safe_free_href(href: str) -> bool: + return (pathutils.is_safe_filesystem_path_component(href) and + not os.path.lexists( + os.path.join(self._filesystem_path, href))) + + def get_safe_free_hrefs(uid: str) -> Iterator[str]: + for href in [uid if uid.lower().endswith(suffix.lower()) + else uid + suffix, + radicale_item.get_etag(uid).strip('"') + suffix]: + if is_safe_free_href(href): + yield href + yield radicale_item.find_available_uid(is_safe_free_href, suffix) - This takes a list of vobject items and - uploads them nonatomic and without existence checks. - - """ cache_folder = os.path.join(self._filesystem_path, ".Radicale.cache", "item") self._storage._makedirs_synced(cache_folder) - hrefs: Set[str] = set() for item in items: uid = item.uid try: @@ -77,39 +84,24 @@ raise ValueError( "Failed to store item %r in temporary collection %r: %s" % (uid, self.path, e)) from e - href_candidate_funtions = [ - lambda: uid if uid.lower().endswith(suffix.lower()) - else uid + suffix, - lambda: radicale_item.get_etag(uid).strip('"') + suffix, - lambda: radicale_item.find_available_uid( - hrefs.__contains__, suffix)] - href = f = None - while href_candidate_funtions: - href = href_candidate_funtions.pop(0)() - if href in hrefs: - continue - if not pathutils.is_safe_filesystem_path_component(href): - if not href_candidate_funtions: - raise pathutils.UnsafePathError(href) - continue + for href in get_safe_free_hrefs(uid): try: - f = open(pathutils.path_to_filesystem( - self._filesystem_path, href), - "w", newline="", encoding=self._encoding) - break + f = open(os.path.join(self._filesystem_path, href), + "w", newline="", encoding=self._encoding) except OSError as e: - if href_candidate_funtions and ( - sys.platform != "win32" and - e.errno == errno.EINVAL or + if (sys.platform != "win32" and e.errno == errno.EINVAL or sys.platform == "win32" and e.errno == 123): + # not a valid filename continue raise - assert href is not None and f is not None + break + else: + raise RuntimeError("No href found for item %r in temporary " + "collection %r" % (uid, self.path)) with f: f.write(item.serialize()) f.flush() self._storage._fsync(f) - hrefs.add(href) with open(os.path.join(cache_folder, href), "wb") as fb: pickle.dump(cache_content, fb) fb.flush() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/tests/static/event_multiple_case_sensitive_uids.ics new/Radicale-3.1.6/radicale/tests/static/event_multiple_case_sensitive_uids.ics --- old/Radicale-3.1.5/radicale/tests/static/event_multiple_case_sensitive_uids.ics 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.1.6/radicale/tests/static/event_multiple_case_sensitive_uids.ics 2022-04-18 23:10:01.000000000 +0200 @@ -0,0 +1,16 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +UID:event +SUMMARY:Event 1 +DTSTART:20130901T190000 +DTEND:20130901T200000 +END:VEVENT +BEGIN:VEVENT +UID:EVENT +SUMMARY:Event 2 +DTSTART:20130901T200000 +DTEND:20130901T210000 +END:VEVENT +END:VCALENDAR diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/tests/test_base.py new/Radicale-3.1.6/radicale/tests/test_base.py --- old/Radicale-3.1.5/radicale/tests/test_base.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/tests/test_base.py 2022-04-18 23:10:01.000000000 +0200 @@ -243,6 +243,13 @@ for uid2 in uids[i + 1:]: assert uid1 != uid2 + def test_put_whole_calendar_case_sensitive_uids(self) -> None: + """Create a whole calendar with case-sensitive UIDs.""" + events = get_file_content("event_multiple_case_sensitive_uids.ics") + self.put("/calendar.ics/", events) + _, answer = self.get("/calendar.ics/") + assert "\r\nUID:event\r\n" in answer and "\r\nUID:EVENT\r\n" in answer + def test_put_whole_addressbook(self) -> None: """Create and overwrite a whole addressbook.""" contacts = get_file_content("contact_multiple.vcf") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/tests/test_server.py new/Radicale-3.1.6/radicale/tests/test_server.py --- old/Radicale-3.1.5/radicale/tests/test_server.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/tests/test_server.py 2022-04-18 23:10:01.000000000 +0200 @@ -28,7 +28,8 @@ import threading import time from configparser import RawConfigParser -from typing import Callable, Dict, NoReturn, Optional, Tuple, cast +from http.client import HTTPMessage +from typing import IO, Callable, Dict, Optional, Tuple, cast from urllib import request from urllib.error import HTTPError, URLError @@ -40,26 +41,10 @@ class DisabledRedirectHandler(request.HTTPRedirectHandler): - - # HACK: typeshed annotation are wrong for `fp` and `msg` - # (https://github.com/python/typeshed/pull/5728) - # `headers` is incompatible with `http.client.HTTPMessage` - # (https://github.com/python/typeshed/issues/5729) - def http_error_301(self, req: request.Request, fp, code: int, - msg, headers) -> NoReturn: - raise HTTPError(req.full_url, code, msg, headers, fp) - - def http_error_302(self, req: request.Request, fp, code: int, - msg, headers) -> NoReturn: - raise HTTPError(req.full_url, code, msg, headers, fp) - - def http_error_303(self, req: request.Request, fp, code: int, - msg, headers) -> NoReturn: - raise HTTPError(req.full_url, code, msg, headers, fp) - - def http_error_307(self, req: request.Request, fp, code: int, - msg, headers) -> NoReturn: - raise HTTPError(req.full_url, code, msg, headers, fp) + def redirect_request( + self, req: request.Request, fp: IO[bytes], code: int, msg: str, + headers: HTTPMessage, newurl: str) -> None: + return None class TestBaseServerRequests(BaseTest): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/utils.py new/Radicale-3.1.6/radicale/utils.py --- old/Radicale-3.1.5/radicale/utils.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/utils.py 2022-04-18 23:10:01.000000000 +0200 @@ -16,12 +16,18 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see <http://www.gnu.org/licenses/>. +import sys from importlib import import_module from typing import Callable, Sequence, Type, TypeVar, Union from radicale import config from radicale.log import logger +if sys.version_info < (3, 8): + import pkg_resources +else: + from importlib import metadata + _T_co = TypeVar("_T_co", covariant=True) @@ -43,3 +49,9 @@ (module_name, module, e)) from e logger.info("%s type is %r", module_name, module) return class_(configuration) + + +def package_version(name): + if sys.version_info < (3, 8): + return pkg_resources.get_distribution(name).version + return metadata.version(name) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/radicale/web/internal.py new/Radicale-3.1.6/radicale/web/internal.py --- old/Radicale-3.1.5/radicale/web/internal.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/radicale/web/internal.py 2022-04-18 23:10:01.000000000 +0200 @@ -25,9 +25,7 @@ """ -import pkg_resources - -from radicale import config, httputils, types, web +from radicale import httputils, types, web MIMETYPES = httputils.MIMETYPES # deprecated FALLBACK_MIMETYPE = httputils.FALLBACK_MIMETYPE # deprecated @@ -35,13 +33,7 @@ class Web(web.BaseWeb): - folder: str - - def __init__(self, configuration: config.Configuration) -> None: - super().__init__(configuration) - self.folder = pkg_resources.resource_filename( - __name__, "internal_data") - def get(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str) -> types.WSGIResponse: - return httputils.serve_folder(self.folder, base_prefix, path) + return httputils.serve_resource("radicale.web", "internal_data", + base_prefix, path) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.1.5/setup.py new/Radicale-3.1.6/setup.py --- old/Radicale-3.1.5/setup.py 2022-02-08 16:44:08.000000000 +0100 +++ new/Radicale-3.1.6/setup.py 2022-04-18 23:10:01.000000000 +0200 @@ -43,12 +43,16 @@ # When the version is updated, a new section in the CHANGELOG.md file must be # added too. -VERSION = "3.1.5" +VERSION = "3.1.6" WEB_FILES = ["web/internal_data/css/icon.png", "web/internal_data/css/main.css", "web/internal_data/fn.js", "web/internal_data/index.html"] +install_requires = ["defusedxml", "passlib", "vobject>=0.9.6", + "python-dateutil>=2.7.3"] +if sys.version_info < (3, 9): + install_requires.append("setuptools") setup_requires = [] if {"pytest", "test", "ptr"}.intersection(sys.argv): setup_requires.append("pytest-runner") @@ -76,8 +80,7 @@ exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), package_data={"radicale": [*WEB_FILES, "py.typed"]}, entry_points={"console_scripts": ["radicale = radicale.__main__:run"]}, - install_requires=["defusedxml", "passlib", "vobject>=0.9.6", - "python-dateutil>=2.7.3", "setuptools"], + install_requires=install_requires, setup_requires=setup_requires, tests_require=tests_require, extras_require={"test": tests_require,
