Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package Radicale for openSUSE:Factory checked in at 2024-09-06 17:18:53 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/Radicale (Old) and /work/SRC/openSUSE:Factory/.Radicale.new.10096 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "Radicale" Fri Sep 6 17:18:53 2024 rev:15 rq:1199117 version:3.2.3 Changes: -------- --- /work/SRC/openSUSE:Factory/Radicale/Radicale.changes 2024-06-27 16:01:00.093520438 +0200 +++ /work/SRC/openSUSE:Factory/.Radicale.new.10096/Radicale.changes 2024-09-06 17:19:16.805723976 +0200 @@ -1,0 +2,16 @@ +Fri Aug 30 04:42:28 UTC 2024 - Ãkos SzÅts <[email protected]> + +- Cleaned up unnecessary BuildRequires +- Update to 3.2.3 + * Add: support for Python 3.13 + * Fix: Using icalendar's tzinfo on created datetime to fix issue with icalendar + * Fix: typos in code + * Enhancement: Added free-busy report + * Enhancement: Added 'max_freebusy_occurrences` setting to avoid potential DOS on reports + * Enhancement: remove unexpected control codes from uploaded items + * Enhancement: add 'strip_domain' setting for username handling + * Enhancement: add option to toggle debug log of rights rule with doesn't match + * Drop: remove unused requirement "typeguard" + * Improve: Refactored some date parsing code + +------------------------------------------------------------------- Old: ---- v3.2.2.tar.gz New: ---- v3.2.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ Radicale.spec ++++++ --- /var/tmp/diff_new_pack.0OfFAq/_old 2024-09-06 17:19:17.333745918 +0200 +++ /var/tmp/diff_new_pack.0OfFAq/_new 2024-09-06 17:19:17.333745918 +0200 @@ -27,7 +27,7 @@ %define du_min_ver 2.7.3 %define pk_min_ver 1.1.0 Name: Radicale -Version: 3.2.2 +Version: 3.2.3 Release: 0 Summary: A CalDAV calendar and CardDav contact server License: GPL-3.0-or-later @@ -42,12 +42,7 @@ BuildRequires: firewall-macros BuildRequires: pkgconfig BuildRequires: python-rpm-macros -BuildRequires: python3-defusedxml -BuildRequires: python3-passlib -BuildRequires: python3-pika >= %{pk_min_ver} -BuildRequires: python3-python-dateutil >= %{du_min_ver} BuildRequires: python3-setuptools -BuildRequires: python3-vobject >= %{vo_min_ver} BuildRequires: systemd-rpm-macros BuildRequires: sysuser-tools BuildRequires: pkgconfig(python3) >= %{py_min_ver} ++++++ v3.2.2.tar.gz -> v3.2.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/.github/workflows/test.yml new/Radicale-3.2.3/.github/workflows/test.yml --- old/Radicale-3.2.2/.github/workflows/test.yml 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/.github/workflows/test.yml 2024-08-30 06:19:51.000000000 +0200 @@ -6,7 +6,7 @@ strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', pypy-3.8, pypy-3.9] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12.3', '3.13.0-beta.4', pypy-3.8, pypy-3.9] exclude: - os: windows-latest python-version: pypy-3.8 @@ -21,7 +21,7 @@ - name: Install Test dependencies run: pip install tox - name: Test - run: tox + run: tox -e py - name: Install Coveralls if: github.event_name == 'push' run: pip install coveralls @@ -46,3 +46,15 @@ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: coveralls --service=github --finish + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install tox + run: pip install tox + - name: Lint + run: tox -e flake8,mypy,isort diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/CHANGELOG.md new/Radicale-3.2.3/CHANGELOG.md --- old/Radicale-3.2.2/CHANGELOG.md 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/CHANGELOG.md 2024-08-30 06:19:51.000000000 +0200 @@ -1,6 +1,16 @@ # Changelog -## 3.dev +## 3.2.3 +* Add: support for Python 3.13 +* Fix: Using icalendar's tzinfo on created datetime to fix issue with icalendar +* Fix: typos in code +* Enhancement: Added free-busy report +* Enhancement: Added 'max_freebusy_occurrences` setting to avoid potential DOS on reports +* Enhancement: remove unexpected control codes from uploaded items +* Enhancement: add 'strip_domain' setting for username handling +* Enhancement: add option to toggle debug log of rights rule with doesn't match +* Drop: remove unused requirement "typeguard" +* Improve: Refactored some date parsing code ## 3.2.2 * Enhancement: add support for auth.type=denyall (will be default for security reasons in upcoming releases) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/DOCUMENTATION.md new/Radicale-3.2.3/DOCUMENTATION.md --- old/Radicale-3.2.2/DOCUMENTATION.md 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/DOCUMENTATION.md 2024-08-30 06:19:51.000000000 +0200 @@ -350,16 +350,13 @@ } ``` -Example **Caddy** configuration with basicauth from Caddy: +Example **Caddy** configuration: -```Caddy -handle_path /radicale* { - basicauth { - user hash - } +``` +handle_path /radicale/* { + uri strip_prefix /radicale reverse_proxy localhost:5232 { - header_up +X-Script-Name "/radicale" - header_up +X-remote-user "{http.auth.user.id}" + header_up X-Script-Name /radicale } } ``` @@ -440,6 +437,21 @@ } ``` +Example **Caddy** configuration: + +``` +handle_path /radicale/* { + uri strip_prefix /radicale + basicauth { + USER HASH + } + reverse_proxy localhost:5232 { + header_up X-Script-Name /radicale + header_up X-remote-user {http.auth.user.id} + } +} +``` + Example **Apache** configuration: ```apache @@ -795,6 +807,12 @@ Default: `False` +##### strip_domain + +Strip domain from username + +Default: `False` + #### rights ##### type @@ -865,7 +883,7 @@ Default: `2592000` -#### skip_broken_item +##### skip_broken_item Skip broken item instead of triggering an exception @@ -960,6 +978,12 @@ Default: `False` +##### rights_rule_doesnt_match_on_debug = True + +Log rights rule which doesn't match on level=debug + +Default: `False` + #### headers In this section additional HTTP headers that are sent to clients can be @@ -1005,6 +1029,18 @@ Default: classic +#### reporting +##### max_freebusy_occurrence + +When returning a free-busy report, a list of busy time occurrences are +generated based on a given time frame. Large time frames could +generate a lot of occurrences based on the time frame supplied. This +setting limits the lookup to prevent potential denial of service +attacks on large time frames. If the limit is reached, an HTTP error +is thrown instead of returning the results. + +Default: 10000 + ## Supported Clients Radicale has been tested with: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/config new/Radicale-3.2.3/config --- old/Radicale-3.2.2/config 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/config 2024-08-30 06:19:51.000000000 +0200 @@ -73,6 +73,8 @@ # Convert username to lowercase, must be true for case-insensitive auth providers #lc_username = False +# Strip domain name from username +#strip_domain = False [rights] @@ -156,6 +158,8 @@ # Log response content on level=debug #response_content_on_debug = False +# Log rights rule which doesn't match on level=debug +#rights_rule_doesnt_match_on_debug = False [headers] @@ -170,3 +174,9 @@ #rabbitmq_endpoint = #rabbitmq_topic = #rabbitmq_queue_type = classic + +[reporting] + +# When returning a free-busy report, limit the number of returned +# occurences per event to prevent DOS attacks. +#max_freebusy_occurrence = 10000 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/contrib/apache/radicale.conf new/Radicale-3.2.3/contrib/apache/radicale.conf --- old/Radicale-3.2.2/contrib/apache/radicale.conf 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/contrib/apache/radicale.conf 2024-08-30 06:19:51.000000000 +0200 @@ -57,13 +57,15 @@ Require all granted </IfDefine> - ## You may want to use apache's authentication (config: [auth] type = remote_user) + ## You may want to use apache's authentication (config: [auth] type = http_x_remote_user) + ## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser #AuthBasicProvider file #AuthType Basic #AuthName "Enter your credentials" - #AuthUserFile /path/to/httpdfile/ + #AuthUserFile /etc/httpd/conf/htpasswd-radicale #AuthGroupFile /dev/null #Require valid-user + #RequestHeader set X-Remote-User expr=%{REMOTE_USER} <IfDefine RADICALE_ENFORCE_SSL> <IfModule !ssl_module> @@ -106,13 +108,15 @@ Require all granted </IfDefine> - ## You may want to use apache's authentication (config: [auth] type = remote_user) + ## You may want to use apache's authentication (config: [auth] type = http_x_remote_user) + ## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser #AuthBasicProvider file #AuthType Basic #AuthName "Enter your credentials" - #AuthUserFile /path/to/httpdfile/ + #AuthUserFile /etc/httpd/conf/htpasswd-radicale #AuthGroupFile /dev/null #Require valid-user + #RequestHeader set X-Remote-User expr=%{REMOTE_USER} <IfDefine RADICALE_ENFORCE_SSL> <IfModule !ssl_module> @@ -179,11 +183,12 @@ Require all granted </IfDefine> - ## You may want to use apache's authentication (config: [auth] type = remote_user) + ## You may want to use apache's authentication (config: [auth] type = http_x_remote_user) + ## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser #AuthBasicProvider file #AuthType Basic #AuthName "Enter your credentials" - #AuthUserFile /path/to/httpdfile/ + #AuthUserFile /etc/httpd/conf/htpasswd-radicale #AuthGroupFile /dev/null #Require valid-user </Location> @@ -221,11 +226,12 @@ Require all granted </IfDefine> - ## You may want to use apache's authentication (config: [auth] type = remote_user) + ## You may want to use apache's authentication (config: [auth] type = http_x_remote_user) + ## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser #AuthBasicProvider file #AuthType Basic #AuthName "Enter your credentials" - #AuthUserFile /path/to/httpdfile/ + #AuthUserFile /etc/httpd/conf/htpasswd-radicale #AuthGroupFile /dev/null #Require valid-user </Location> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/__init__.py new/Radicale-3.2.3/radicale/__init__.py --- old/Radicale-3.2.2/radicale/__init__.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/__init__.py 2024-08-30 06:19:51.000000000 +0200 @@ -61,7 +61,7 @@ if not miss and source != "default config": default_config_active = False if default_config_active: - logger.warn("%s", "No config file found/readable - only default config is active") + logger.warning("%s", "No config file found/readable - only default config is active") _application_instance = Application(configuration) if _application_config_path != config_path: raise ValueError("RADICALE_CONFIG must not change: %r != %r" % diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/__main__.py new/Radicale-3.2.3/radicale/__main__.py --- old/Radicale-3.2.2/radicale/__main__.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/__main__.py 2024-08-30 06:19:51.000000000 +0200 @@ -175,7 +175,7 @@ default_config_active = False if default_config_active: - logger.warn("%s", "No config file found/readable - only default config is active") + logger.warning("%s", "No config file found/readable - only default config is active") if args_ns.verify_storage: logger.info("Verifying storage") @@ -183,7 +183,7 @@ storage_ = storage.load(configuration) with storage_.acquire_lock("r"): if not storage_.verify(): - logger.critical("Storage verifcation failed") + logger.critical("Storage verification failed") sys.exit(1) except Exception as e: logger.critical("An exception occurred during storage " diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/__init__.py new/Radicale-3.2.3/radicale/app/__init__.py --- old/Radicale-3.2.2/radicale/app/__init__.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/app/__init__.py 2024-08-30 06:19:51.000000000 +0200 @@ -146,7 +146,7 @@ if self._response_content_on_debug: logger.debug("Response content:\n%s", answer) else: - logger.debug("Response content: suppressed by config/option [auth] response_content_on_debug") + logger.debug("Response content: suppressed by config/option [logging] response_content_on_debug") headers["Content-Type"] += "; charset=%s" % self._encoding answer = answer.encode(self._encoding) accept_encoding = [ @@ -196,7 +196,7 @@ logger.debug("Request header:\n%s", pprint.pformat(self._scrub_headers(environ))) else: - logger.debug("Request header: suppressed by config/option [auth] request_header_on_debug") + logger.debug("Request header: suppressed by config/option [logging] request_header_on_debug") # SCRIPT_NAME is already removed from PATH_INFO, according to the # WSGI specification. @@ -232,7 +232,7 @@ path.rstrip("/").endswith("/.well-known/carddav")): return response(*httputils.redirect( base_prefix + "/", client.MOVED_PERMANENTLY)) - # Return NOT FOUND for all other paths containing ".well-knwon" + # Return NOT FOUND for all other paths containing ".well-known" if path.endswith("/.well-known") or "/.well-known/" in path: return response(*httputils.NOT_FOUND) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/base.py new/Radicale-3.2.3/radicale/app/base.py --- old/Radicale-3.2.2/radicale/app/base.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/app/base.py 2024-08-30 06:19:51.000000000 +0200 @@ -51,6 +51,7 @@ self._encoding = configuration.get("encoding", "request") self._log_bad_put_request_content = configuration.get("logging", "bad_put_request_content") self._response_content_on_debug = configuration.get("logging", "response_content_on_debug") + self._request_content_on_debug = configuration.get("logging", "request_content_on_debug") self._hook = hook.load(configuration) def _read_xml_request_body(self, environ: types.WSGIEnviron @@ -66,17 +67,20 @@ logger.debug("Request content (Invalid XML):\n%s", content) raise RuntimeError("Failed to parse XML: %s" % e) from e if logger.isEnabledFor(logging.DEBUG): - logger.debug("Request content:\n%s", - xmlutils.pretty_xml(xml_content)) + if self._request_content_on_debug: + logger.debug("Request content (XML):\n%s", + xmlutils.pretty_xml(xml_content)) + else: + logger.debug("Request content (XML): suppressed by config/option [logging] request_content_on_debug") return xml_content def _xml_response(self, xml_content: ET.Element) -> bytes: if logger.isEnabledFor(logging.DEBUG): if self._response_content_on_debug: - logger.debug("Response content:\n%s", + logger.debug("Response content (XML):\n%s", xmlutils.pretty_xml(xml_content)) else: - logger.debug("Response content: suppressed by config/option [auth] response_content_on_debug") + logger.debug("Response content (XML): suppressed by config/option [logging] response_content_on_debug") f = io.BytesIO() ET.ElementTree(xml_content).write(f, encoding=self._encoding, xml_declaration=True) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/propfind.py new/Radicale-3.2.3/radicale/app/propfind.py --- old/Radicale-3.2.2/radicale/app/propfind.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/app/propfind.py 2024-08-30 06:19:51.000000000 +0200 @@ -322,13 +322,13 @@ responses[404 if is404 else 200].append(element) - for status_code, childs in responses.items(): - if not childs: + for status_code, children in responses.items(): + if not children: continue propstat = ET.Element(xmlutils.make_clark("D:propstat")) response.append(propstat) prop = ET.Element(xmlutils.make_clark("D:prop")) - prop.extend(childs) + prop.extend(children) propstat.append(prop) status = ET.Element(xmlutils.make_clark("D:status")) status.text = xmlutils.make_response(status_code) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/put.py new/Radicale-3.2.3/radicale/app/put.py --- old/Radicale-3.2.2/radicale/app/put.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/app/put.py 2024-08-30 06:19:51.000000000 +0200 @@ -150,7 +150,7 @@ if self._log_bad_put_request_content: logger.warning("Bad PUT request content of %r:\n%s", path, content) else: - logger.debug("Bad PUT request content: suppressed by config/option [auth] bad_put_request_content") + logger.debug("Bad PUT request content: suppressed by config/option [logging] bad_put_request_content") return httputils.BAD_REQUEST (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/report.py new/Radicale-3.2.3/radicale/app/report.py --- old/Radicale-3.2.2/radicale/app/report.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/app/report.py 2024-08-30 06:19:51.000000000 +0200 @@ -28,6 +28,7 @@ Sequence, Tuple, Union) from urllib.parse import unquote, urlparse +import vobject import vobject.base from vobject.base import ContentLine @@ -38,11 +39,110 @@ from radicale.log import logger +def free_busy_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], + collection: storage.BaseCollection, encoding: str, + unlock_storage_fn: Callable[[], None], + max_occurrence: int + ) -> Tuple[int, Union[ET.Element, str]]: + # NOTE: this function returns both an Element and a string because + # free-busy reports are an edge-case on the return type according + # to the spec. + + multistatus = ET.Element(xmlutils.make_clark("D:multistatus")) + if xml_request is None: + return client.MULTI_STATUS, multistatus + root = xml_request + if (root.tag == xmlutils.make_clark("C:free-busy-query") and + collection.tag != "VCALENDAR"): + logger.warning("Invalid REPORT method %r on %r requested", + xmlutils.make_human_tag(root.tag), path) + return client.FORBIDDEN, xmlutils.webdav_error("D:supported-report") + + time_range_element = root.find(xmlutils.make_clark("C:time-range")) + assert isinstance(time_range_element, ET.Element) + + # Build a single filter from the free busy query for retrieval + # TODO: filter for VFREEBUSY in additional to VEVENT but + # test_filter doesn't support that yet. + vevent_cf_element = ET.Element(xmlutils.make_clark("C:comp-filter"), + attrib={'name': 'VEVENT'}) + vevent_cf_element.append(time_range_element) + vcalendar_cf_element = ET.Element(xmlutils.make_clark("C:comp-filter"), + attrib={'name': 'VCALENDAR'}) + vcalendar_cf_element.append(vevent_cf_element) + filter_element = ET.Element(xmlutils.make_clark("C:filter")) + filter_element.append(vcalendar_cf_element) + filters = (filter_element,) + + # First pull from storage + retrieved_items = list(collection.get_filtered(filters)) + # !!! Don't access storage after this !!! + unlock_storage_fn() + + cal = vobject.iCalendar() + collection_tag = collection.tag + while retrieved_items: + # Second filtering before evaluating occurrences. + # ``item.vobject_item`` might be accessed during filtering. + # Don't keep reference to ``item``, because VObject requires a lot of + # memory. + item, filter_matched = retrieved_items.pop(0) + if not filter_matched: + try: + if not test_filter(collection_tag, item, filter_element): + continue + except ValueError as e: + raise ValueError("Failed to free-busy filter item %r from %r: %s" % + (item.href, collection.path, e)) from e + except Exception as e: + raise RuntimeError("Failed to free-busy filter item %r from %r: %s" % + (item.href, collection.path, e)) from e + + fbtype = None + if item.component_name == 'VEVENT': + transp = getattr(item.vobject_item.vevent, 'transp', None) + if transp and transp.value != 'OPAQUE': + continue + + status = getattr(item.vobject_item.vevent, 'status', None) + if not status or status.value == 'CONFIRMED': + fbtype = 'BUSY' + elif status.value == 'CANCELLED': + fbtype = 'FREE' + elif status.value == 'TENTATIVE': + fbtype = 'BUSY-TENTATIVE' + else: + # Could do fbtype = status.value for x-name, I prefer this + fbtype = 'BUSY' + + # TODO: coalesce overlapping periods + + if max_occurrence > 0: + n_occurrences = max_occurrence+1 + else: + n_occurrences = 0 + occurrences = radicale_filter.time_range_fill(item.vobject_item, + time_range_element, + "VEVENT", + n=n_occurrences) + if len(occurrences) >= max_occurrence: + raise ValueError("FREEBUSY occurrences limit of {} hit" + .format(max_occurrence)) + + for occurrence in occurrences: + vfb = cal.add('vfreebusy') + vfb.add('dtstamp').value = item.vobject_item.vevent.dtstamp.value + vfb.add('dtstart').value, vfb.add('dtend').value = occurrence + if fbtype: + vfb.add('fbtype').value = fbtype + return (client.OK, cal.serialize()) + + def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], collection: storage.BaseCollection, encoding: str, unlock_storage_fn: Callable[[], None] ) -> Tuple[int, ET.Element]: - """Read and answer REPORT requests. + """Read and answer REPORT requests that return XML. Read rfc3253-3.6 for info. @@ -271,7 +371,7 @@ if hasattr(item.vobject_item.vevent, 'rrule'): rruleset = vevent.getrruleset() - # There is something strage behavour during serialization native datetime, so converting manualy + # There is something strange behaviour during serialization native datetime, so converting manually vevent.dtstart.value = vevent.dtstart.value.strftime(dt_format) if dt_end is not None: vevent.dtend.value = vevent.dtend.value.strftime(dt_format) @@ -426,13 +526,28 @@ else: assert item.collection is not None collection = item.collection - try: - status, xml_answer = xml_report( - base_prefix, path, xml_content, collection, self._encoding, - lock_stack.close) - except ValueError as e: - logger.warning( - "Bad REPORT request on %r: %s", path, e, exc_info=True) - return httputils.BAD_REQUEST - headers = {"Content-Type": "text/xml; charset=%s" % self._encoding} - return status, headers, self._xml_response(xml_answer) + + if xml_content is not None and \ + xml_content.tag == xmlutils.make_clark("C:free-busy-query"): + max_occurrence = self.configuration.get("reporting", "max_freebusy_occurrence") + try: + status, body = free_busy_report( + base_prefix, path, xml_content, collection, self._encoding, + lock_stack.close, max_occurrence) + except ValueError as e: + logger.warning( + "Bad REPORT request on %r: %s", path, e, exc_info=True) + return httputils.BAD_REQUEST + headers = {"Content-Type": "text/calendar; charset=%s" % self._encoding} + return status, headers, str(body) + else: + try: + status, xml_answer = xml_report( + base_prefix, path, xml_content, collection, self._encoding, + lock_stack.close) + except ValueError as e: + logger.warning( + "Bad REPORT request on %r: %s", path, e, exc_info=True) + return httputils.BAD_REQUEST + headers = {"Content-Type": "text/xml; charset=%s" % self._encoding} + return status, headers, self._xml_response(xml_answer) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/auth/__init__.py new/Radicale-3.2.3/radicale/auth/__init__.py --- old/Radicale-3.2.2/radicale/auth/__init__.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/auth/__init__.py 2024-08-30 06:19:51.000000000 +0200 @@ -52,6 +52,7 @@ class BaseAuth: _lc_username: bool + _strip_domain: bool def __init__(self, configuration: "config.Configuration") -> None: """Initialize BaseAuth. @@ -63,6 +64,7 @@ """ self.configuration = configuration self._lc_username = configuration.get("auth", "lc_username") + self._strip_domain = configuration.get("auth", "strip_domain") def get_external_login(self, environ: types.WSGIEnviron) -> Union[ Tuple[()], Tuple[str, str]]: @@ -91,4 +93,8 @@ raise NotImplementedError def login(self, login: str, password: str) -> str: - return self._login(login, password).lower() if self._lc_username else self._login(login, password) + if self._lc_username: + login = login.lower() + if self._strip_domain: + login = login.split('@')[0] + return self._login(login, password) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/auth/htpasswd.py new/Radicale-3.2.3/radicale/auth/htpasswd.py --- old/Radicale-3.2.2/radicale/auth/htpasswd.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/auth/htpasswd.py 2024-08-30 06:19:51.000000000 +0200 @@ -36,7 +36,7 @@ the password encryption method specified via the ``htpasswd_encryption`` configuration value. -The following htpasswd password encrpytion methods are supported by Radicale +The following htpasswd password encryption methods are supported by Radicale out-of-the-box: - plain-text (created by htpasswd -p ...) -- INSECURE - MD5-APR1 (htpasswd -m ...) -- htpasswd's default method, INSECURE diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/config.py new/Radicale-3.2.3/radicale/config.py --- old/Radicale-3.2.2/radicale/config.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/config.py 2024-08-30 06:19:51.000000000 +0200 @@ -191,6 +191,10 @@ "value": "1", "help": "incorrect authentication delay", "type": positive_float}), + ("strip_domain", { + "value": "False", + "help": "strip domain from username", + "type": bool}), ("lc_username", { "value": "False", "help": "convert username to lowercase, must be true for case-insensitive auth providers", @@ -288,12 +292,22 @@ "value": "False", "help": "log response content on level=debug", "type": bool}), + ("rights_rule_doesnt_match_on_debug", { + "value": "False", + "help": "log rights rules which doesn't match on level=debug", + "type": bool}), ("mask_passwords", { "value": "True", "help": "mask passwords in logs", "type": bool})])), ("headers", OrderedDict([ - ("_allow_extra", str)]))]) + ("_allow_extra", str)])), + ("reporting", OrderedDict([ + ("max_freebusy_occurrence", { + "value": "10000", + "help": "number of occurrences per event when reporting", + "type": positive_int})])) + ]) def parse_compound_paths(*compound_paths: Optional[str] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/hook/__init__.py new/Radicale-3.2.3/radicale/hook/__init__.py --- old/Radicale-3.2.2/radicale/hook/__init__.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/hook/__init__.py 2024-08-30 06:19:51.000000000 +0200 @@ -3,14 +3,23 @@ from typing import Sequence from radicale import pathutils, utils +from radicale.log import logger INTERNAL_TYPES: Sequence[str] = ("none", "rabbitmq") def load(configuration): """Load the storage module chosen in configuration.""" - return utils.load_plugin( - INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration) + try: + return utils.load_plugin( + INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration) + except Exception as e: + logger.warning(e) + logger.warning("Hook \"%s\" failed to load, falling back to \"none\"." % configuration.get("hook", "type")) + configuration = configuration.copy() + configuration.update({"hook": {"type": "none"}}, "hook", privileged=True) + return utils.load_plugin( + INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration) class BaseHook: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/httputils.py new/Radicale-3.2.3/radicale/httputils.py --- old/Radicale-3.2.2/radicale/httputils.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/httputils.py 2024-08-30 06:19:51.000000000 +0200 @@ -146,7 +146,7 @@ if configuration.get("logging", "request_content_on_debug"): logger.debug("Request content:\n%s", content) else: - logger.debug("Request content: suppressed by config/option [auth] request_content_on_debug") + logger.debug("Request content: suppressed by config/option [logging] request_content_on_debug") return content diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/item/__init__.py new/Radicale-3.2.3/radicale/item/__init__.py --- old/Radicale-3.2.2/radicale/item/__init__.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/item/__init__.py 2024-08-30 06:19:51.000000000 +0200 @@ -49,6 +49,12 @@ s = re.sub(r"^(PHOTO(?:;[^:\r\n]*)?;ENCODING=b(?:;[^:\r\n]*)?:)" r"data:[^;,\r\n]*;base64,", r"\1", s, flags=re.MULTILINE | re.IGNORECASE) + # Workaround for bug with malformed ICS files containing control codes + # Filter out all control codes except those we expect to find: + # * 0x09 Horizontal Tab + # * 0x0A Line Feed + # * 0x0D Carriage Return + s = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', s) return list(vobject.readComponents(s, allowQP=True)) @@ -298,7 +304,7 @@ Returns a tuple (``start``, ``end``) where ``start`` and ``end`` are POSIX timestamps. - This is intened to be used for matching against simplified prefilters. + This is intended to be used for matching against simplified prefilters. """ if not tag: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/item/filter.py new/Radicale-3.2.3/radicale/item/filter.py --- old/Radicale-3.2.2/radicale/item/filter.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/item/filter.py 2024-08-30 06:19:51.000000000 +0200 @@ -48,10 +48,34 @@ if not isinstance(d, datetime): d = datetime.combine(d, datetime.min.time()) if not d.tzinfo: - d = d.replace(tzinfo=timezone.utc) + # NOTE: using vobject's UTC as it wasn't playing well with datetime's. + d = d.replace(tzinfo=vobject.icalendar.utc) return d +def parse_time_range(time_filter: ET.Element) -> Tuple[datetime, datetime]: + start_text = time_filter.get("start") + end_text = time_filter.get("end") + if start_text: + start = datetime.strptime( + start_text, "%Y%m%dT%H%M%SZ").replace( + tzinfo=timezone.utc) + else: + start = DATETIME_MIN + if end_text: + end = datetime.strptime( + end_text, "%Y%m%dT%H%M%SZ").replace( + tzinfo=timezone.utc) + else: + end = DATETIME_MAX + return start, end + + +def time_range_timestamps(time_filter: ET.Element) -> Tuple[int, int]: + start, end = parse_time_range(time_filter) + return (math.floor(start.timestamp()), math.ceil(end.timestamp())) + + def comp_match(item: "item.Item", filter_: ET.Element, level: int = 0) -> bool: """Check whether the ``item`` matches the comp ``filter_``. @@ -147,21 +171,10 @@ """Check whether the component/property ``child_name`` of ``vobject_item`` matches the time-range ``filter_``.""" - start_text = filter_.get("start") - end_text = filter_.get("end") - if not start_text and not end_text: + if not filter_.get("start") and not filter_.get("end"): return False - if start_text: - start = datetime.strptime(start_text, "%Y%m%dT%H%M%SZ") - else: - start = datetime.min - if end_text: - end = datetime.strptime(end_text, "%Y%m%dT%H%M%SZ") - else: - end = datetime.max - start = start.replace(tzinfo=timezone.utc) - end = end.replace(tzinfo=timezone.utc) + start, end = parse_time_range(filter_) matched = False def range_fn(range_start: datetime, range_end: datetime, @@ -181,6 +194,35 @@ return matched +def time_range_fill(vobject_item: vobject.base.Component, + filter_: ET.Element, child_name: str, n: int = 1 + ) -> List[Tuple[datetime, datetime]]: + """Create a list of ``n`` occurances from the component/property ``child_name`` + of ``vobject_item``.""" + if not filter_.get("start") and not filter_.get("end"): + return [] + + start, end = parse_time_range(filter_) + ranges: List[Tuple[datetime, datetime]] = [] + + def range_fn(range_start: datetime, range_end: datetime, + is_recurrence: bool) -> bool: + nonlocal ranges + if start < range_end and range_start < end: + ranges.append((range_start, range_end)) + if n > 0 and len(ranges) >= n: + return True + if end < range_start and not is_recurrence: + return True + return False + + def infinity_fn(range_start: datetime) -> bool: + return False + + visit_time_ranges(vobject_item, child_name, range_fn, infinity_fn) + return ranges + + def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str, range_fn: Callable[[datetime, datetime, bool], bool], infinity_fn: Callable[[datetime], bool]) -> None: @@ -199,7 +241,7 @@ """ - # HACK: According to rfc5545-3.8.4.4 an recurrance that is resheduled + # HACK: According to rfc5545-3.8.4.4 a recurrence that is rescheduled # with Recurrence ID affects the recurrence itself and all following # recurrences too. This is not respected and client don't seem to bother # either. @@ -543,20 +585,7 @@ if time_filter.tag != xmlutils.make_clark("C:time-range"): simple = False continue - start_text = time_filter.get("start") - end_text = time_filter.get("end") - if start_text: - start = math.floor(datetime.strptime( - start_text, "%Y%m%dT%H%M%SZ").replace( - tzinfo=timezone.utc).timestamp()) - else: - start = TIMESTAMP_MIN - if end_text: - end = math.ceil(datetime.strptime( - end_text, "%Y%m%dT%H%M%SZ").replace( - tzinfo=timezone.utc).timestamp()) - else: - end = TIMESTAMP_MAX + start, end = time_range_timestamps(time_filter) return tag, start, end, simple return tag, TIMESTAMP_MIN, TIMESTAMP_MAX, simple return None, TIMESTAMP_MIN, TIMESTAMP_MAX, simple diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/rights/from_file.py new/Radicale-3.2.3/radicale/rights/from_file.py --- old/Radicale-3.2.2/radicale/rights/from_file.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/rights/from_file.py 2024-08-30 06:19:51.000000000 +0200 @@ -22,7 +22,7 @@ The login is matched against the "user" key, and the collection path is matched against the "collection" key. In the "collection" regex you can use `{user}` and get groups from the "user" regex with `{0}`, `{1}`, etc. -In consequence of the parameter subsitution you have to write `{{` and `}}` +In consequence of the parameter substitution you have to write `{{` and `}}` if you want to use regular curly braces in the "user" and "collection" regexes. For example, for the "user" key, ".+" means "authenticated user" and ".*" @@ -48,6 +48,7 @@ def __init__(self, configuration: config.Configuration) -> None: super().__init__(configuration) self._filename = configuration.get("rights", "file") + self._log_rights_rule_doesnt_match_on_debug = configuration.get("logging", "rights_rule_doesnt_match_on_debug") def authorization(self, user: str, path: str) -> str: user = user or "" @@ -61,6 +62,8 @@ except Exception as e: raise RuntimeError("Failed to load rights file %r: %s" % (self._filename, e)) from e + if not self._log_rights_rule_doesnt_match_on_debug: + logger.debug("logging of rules which doesn't match suppressed by config/option [logging] rights_rule_doesnt_match_on_debug") for section in rights_config.sections(): try: user_pattern = rights_config.get(section, "user") @@ -80,8 +83,9 @@ user, sane_path, user_pattern, collection_pattern, section, permission) return permission - logger.debug("Rule %r:%r doesn't match %r:%r from section %r", - user, sane_path, user_pattern, collection_pattern, - section) + if self._log_rights_rule_doesnt_match_on_debug: + logger.debug("Rule %r:%r doesn't match %r:%r from section %r", + user, sane_path, user_pattern, collection_pattern, + section) logger.info("Rights: %r:%r doesn't match any section", user, sane_path) return "" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/server.py new/Radicale-3.2.3/radicale/server.py --- old/Radicale-3.2.2/radicale/server.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/server.py 2024-08-30 06:19:51.000000000 +0200 @@ -291,7 +291,7 @@ try: getaddrinfo = socket.getaddrinfo(address_port[0], address_port[1], 0, socket.SOCK_STREAM, socket.IPPROTO_TCP) except OSError as e: - logger.warn("cannot retrieve IPv4 or IPv6 address of '%s': %s" % (format_address(address_port), e)) + logger.warning("cannot retrieve IPv4 or IPv6 address of '%s': %s" % (format_address(address_port), e)) continue logger.debug("getaddrinfo of '%s': %s" % (format_address(address_port), getaddrinfo)) for (address_family, socket_kind, socket_proto, socket_flags, socket_address) in getaddrinfo: @@ -299,7 +299,7 @@ try: server = server_class(configuration, address_family, (socket_address[0], socket_address[1]), RequestHandler) except OSError as e: - logger.warn("cannot create server socket on '%s': %s" % (format_address(socket_address), e)) + logger.warning("cannot create server socket on '%s': %s" % (format_address(socket_address), e)) continue servers[server.socket] = server server.set_app(application) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/storage/multifilesystem/get.py new/Radicale-3.2.3/radicale/storage/multifilesystem/get.py --- old/Radicale-3.2.2/radicale/storage/multifilesystem/get.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/storage/multifilesystem/get.py 2024-08-30 06:19:51.000000000 +0200 @@ -84,7 +84,7 @@ cache_content = self._load_item_cache(href, cache_hash) if cache_content is None: with self._acquire_cache_lock("item"): - # Lock the item cache to prevent multpile processes from + # Lock the item cache to prevent multiple processes from # generating the same data in parallel. # This improves the performance for multiple requests. if self._storage._lock.locked == "r": @@ -127,7 +127,7 @@ def get_multi(self, hrefs: Iterable[str] ) -> Iterator[Tuple[str, Optional[radicale_item.Item]]]: - # It's faster to check for file name collissions here, because + # It's faster to check for file name collisions here, because # we only need to call os.listdir once. files = None for href in hrefs: @@ -146,7 +146,7 @@ def get_all(self) -> Iterator[radicale_item.Item]: for href in self._list(): - # We don't need to check for collissions, because the file names + # We don't need to check for collisions, because the file names # are from os.listdir. item = self._get(href, verify_href=False) if item is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/tests/__init__.py new/Radicale-3.2.3/radicale/tests/__init__.py --- old/Radicale-3.2.2/radicale/tests/__init__.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/tests/__init__.py 2024-08-30 06:19:51.000000000 +0200 @@ -31,11 +31,12 @@ from typing import Any, Dict, List, Optional, Tuple, Union import defusedxml.ElementTree as DefusedET +import vobject import radicale from radicale import app, config, types, xmlutils -RESPONSES = Dict[str, Union[int, Dict[str, Tuple[int, ET.Element]]]] +RESPONSES = Dict[str, Union[int, Dict[str, Tuple[int, ET.Element]], vobject.base.Component]] # Enable debug output radicale.log.logger.setLevel(logging.DEBUG) @@ -107,12 +108,11 @@ def parse_responses(text: str) -> RESPONSES: xml = DefusedET.fromstring(text) assert xml.tag == xmlutils.make_clark("D:multistatus") - path_responses: Dict[str, Union[ - int, Dict[str, Tuple[int, ET.Element]]]] = {} + path_responses: RESPONSES = {} for response in xml.findall(xmlutils.make_clark("D:response")): href = response.find(xmlutils.make_clark("D:href")) assert href.text not in path_responses - prop_respones: Dict[str, Tuple[int, ET.Element]] = {} + prop_responses: Dict[str, Tuple[int, ET.Element]] = {} for propstat in response.findall( xmlutils.make_clark("D:propstat")): status = propstat.find(xmlutils.make_clark("D:status")) @@ -121,16 +121,22 @@ for element in propstat.findall( "./%s/*" % xmlutils.make_clark("D:prop")): human_tag = xmlutils.make_human_tag(element.tag) - assert human_tag not in prop_respones - prop_respones[human_tag] = (status_code, element) + assert human_tag not in prop_responses + prop_responses[human_tag] = (status_code, element) status = response.find(xmlutils.make_clark("D:status")) if status is not None: - assert not prop_respones + assert not prop_responses assert status.text.startswith("HTTP/1.1 ") status_code = int(status.text.split(" ")[1]) path_responses[href.text] = status_code else: - path_responses[href.text] = prop_respones + path_responses[href.text] = prop_responses + return path_responses + + @staticmethod + def parse_free_busy(text: str) -> RESPONSES: + path_responses: RESPONSES = {} + path_responses[""] = vobject.readOne(text) return path_responses def get(self, path: str, check: Optional[int] = 200, **kwargs @@ -177,13 +183,18 @@ return status, responses def report(self, path: str, data: str, check: Optional[int] = 207, + is_xml: Optional[bool] = True, **kwargs) -> Tuple[int, RESPONSES]: status, _, answer = self.request("REPORT", path, data, check=check, **kwargs) if status < 200 or 300 <= status: return status, {} assert answer is not None - return status, self.parse_responses(answer) + if is_xml: + parsed = self.parse_responses(answer) + else: + parsed = self.parse_free_busy(answer) + return status, parsed def delete(self, path: str, check: Optional[int] = 200, **kwargs ) -> Tuple[int, RESPONSES]: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/tests/static/event10.ics new/Radicale-3.2.3/radicale/tests/static/event10.ics --- old/Radicale-3.2.2/radicale/tests/static/event10.ics 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.2.3/radicale/tests/static/event10.ics 2024-08-30 06:19:51.000000000 +0200 @@ -0,0 +1,36 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/Paris +X-LIC-LOCATION:Europe/Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20130902T150157Z +LAST-MODIFIED:20130902T150158Z +DTSTAMP:20130902T150158Z +UID:event10 +SUMMARY:Event +CATEGORIES:some_category1,another_category2 +ORGANIZER:mailto:[email protected] +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Jane Doe:MAILTO:[email protected] +ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="MAILTO:[email protected]";PARTSTAT=ACCEPTED;CN=John Doe:MAILTO:[email protected] +DTSTART;TZID=Europe/Paris:20130901T180000 +DTEND;TZID=Europe/Paris:20130901T190000 +STATUS:CANCELLED +END:VEVENT +END:VCALENDAR diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/tests/test_auth.py new/Radicale-3.2.3/radicale/tests/test_auth.py --- old/Radicale-3.2.2/radicale/tests/test_auth.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/tests/test_auth.py 2024-08-30 06:19:51.000000000 +0200 @@ -115,6 +115,16 @@ def test_htpasswd_comment(self) -> None: self._test_htpasswd("plain", "#comment\n #comment\n \ntmp:bepo\n\n") + def test_htpasswd_lc_username(self) -> None: + self.configure({"auth": {"lc_username": "True"}}) + self._test_htpasswd("plain", "tmp:bepo", ( + ("tmp", "bepo", True), ("TMP", "bepo", True), ("tmp1", "bepo", False))) + + def test_htpasswd_strip_domain(self) -> None: + self.configure({"auth": {"strip_domain": "True"}}) + self._test_htpasswd("plain", "tmp:bepo", ( + ("tmp", "bepo", True), ("[email protected]", "bepo", True), ("tmp1", "bepo", False))) + def test_remote_user(self) -> None: self.configure({"auth": {"type": "remote_user"}}) _, responses = self.propfind("/", """\ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/radicale/tests/test_base.py new/Radicale-3.2.3/radicale/tests/test_base.py --- old/Radicale-3.2.2/radicale/tests/test_base.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/radicale/tests/test_base.py 2024-08-30 06:19:51.000000000 +0200 @@ -25,6 +25,7 @@ from typing import Any, Callable, ClassVar, Iterable, List, Optional, Tuple import defusedxml.ElementTree as DefusedET +import vobject from radicale import storage, xmlutils from radicale.tests import RESPONSES, BaseTest @@ -359,7 +360,7 @@ self.get(path1, check=404) self.get(path2) - def test_move_between_colections(self) -> None: + def test_move_between_collections(self) -> None: """Move a item.""" self.mkcalendar("/calendar1.ics/") self.mkcalendar("/calendar2.ics/") @@ -372,7 +373,7 @@ self.get(path1, check=404) self.get(path2) - def test_move_between_colections_duplicate_uid(self) -> None: + def test_move_between_collections_duplicate_uid(self) -> None: """Move a item to a collection which already contains the UID.""" self.mkcalendar("/calendar1.ics/") self.mkcalendar("/calendar2.ics/") @@ -388,7 +389,7 @@ assert xml.tag == xmlutils.make_clark("D:error") assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None - def test_move_between_colections_overwrite(self) -> None: + def test_move_between_collections_overwrite(self) -> None: """Move a item to a collection which already contains the item.""" self.mkcalendar("/calendar1.ics/") self.mkcalendar("/calendar2.ics/") @@ -402,8 +403,8 @@ self.request("MOVE", path1, check=204, HTTP_OVERWRITE="T", HTTP_DESTINATION="http://127.0.0.1/"+path2) - def test_move_between_colections_overwrite_uid_conflict(self) -> None: - """Move a item to a collection which already contains the item with + def test_move_between_collections_overwrite_uid_conflict(self) -> None: + """Move an item to a collection which already contains the item with a different UID.""" self.mkcalendar("/calendar1.ics/") self.mkcalendar("/calendar2.ics/") @@ -1360,10 +1361,45 @@ </C:calendar-query>""") assert len(responses) == 1 response = responses[event_path] - assert not isinstance(response, int) + assert isinstance(response, dict) status, prop = response["D:getetag"] assert status == 200 and prop.text + def test_report_free_busy(self) -> None: + """Test free busy report on a few items""" + calendar_path = "/calendar.ics/" + self.mkcalendar(calendar_path) + for i in (1, 2, 10): + filename = "event{}.ics".format(i) + event = get_file_content(filename) + self.put(posixpath.join(calendar_path, filename), event) + code, responses = self.report(calendar_path, """\ +<?xml version="1.0" encoding="utf-8" ?> +<C:free-busy-query xmlns:C="urn:ietf:params:xml:ns:caldav"> + <C:time-range start="20130901T140000Z" end="20130908T220000Z"/> +</C:free-busy-query>""", 200, is_xml=False) + for response in responses.values(): + assert isinstance(response, vobject.base.Component) + assert len(responses) == 1 + vcalendar = list(responses.values())[0] + assert isinstance(vcalendar, vobject.base.Component) + assert len(vcalendar.vfreebusy_list) == 3 + types = {} + for vfb in vcalendar.vfreebusy_list: + fbtype_val = vfb.fbtype.value + if fbtype_val not in types: + types[fbtype_val] = 0 + types[fbtype_val] += 1 + assert types == {'BUSY': 2, 'FREE': 1} + + # Test max_freebusy_occurrence limit + self.configure({"reporting": {"max_freebusy_occurrence": 1}}) + code, responses = self.report(calendar_path, """\ +<?xml version="1.0" encoding="utf-8" ?> +<C:free-busy-query xmlns:C="urn:ietf:params:xml:ns:caldav"> + <C:time-range start="20130901T140000Z" end="20130908T220000Z"/> +</C:free-busy-query>""", 400, is_xml=False) + def _report_sync_token( self, calendar_path: str, sync_token: Optional[str] = None ) -> Tuple[str, RESPONSES]: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/setup.cfg new/Radicale-3.2.3/setup.cfg --- old/Radicale-3.2.2/setup.cfg 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/setup.cfg 2024-08-30 06:19:51.000000000 +0200 @@ -1,26 +1,31 @@ [tool:pytest] -addopts = --typeguard-packages=radicale [tox:tox] +min_version = 4.0 +envlist = py, flake8, isort, mypy [testenv] -extras = test +extras = + test deps = - flake8 - isort - # mypy installation fails with pypy<3.9 - mypy; implementation_name!='pypy' or python_version>='3.9' - types-setuptools + pytest pytest-cov -commands = - flake8 . - isort --check --diff . - # Run mypy if it's installed - python -c 'import importlib.util, subprocess, sys; \ - importlib.util.find_spec("mypy") \ - and sys.exit(subprocess.run(["mypy", "."]).returncode) \ - or print("Skipped: mypy is not installed")' - pytest -r s --cov --cov-report=term --cov-report=xml . +commands = pytest -r s --cov --cov-report=term --cov-report=xml . + +[testenv:flake8] +deps = flake8==7.1.0 +commands = flake8 . +skip_install = True + +[testenv:isort] +deps = isort==5.13.2 +commands = isort --check --diff . +skip_install = True + +[testenv:mypy] +deps = mypy==1.11.0 +commands = mypy . +skip_install = True [tool:isort] known_standard_library = _dummy_thread,_thread,abc,aifc,argparse,array,ast,asynchat,asyncio,asyncore,atexit,audioop,base64,bdb,binascii,binhex,bisect,builtins,bz2,cProfile,calendar,cgi,cgitb,chunk,cmath,cmd,code,codecs,codeop,collections,colorsys,compileall,concurrent,configparser,contextlib,contextvars,copy,copyreg,crypt,csv,ctypes,curses,dataclasses,datetime,dbm,decimal,difflib,dis,distutils,doctest,dummy_threading,email,encodings,ensurepip,enum,errno,faulthandler,fcntl,filecmp,fileinput,fnmatch,formatter,fpectl,fractions,ftplib,functools,gc,getopt,getpass,gettext,glob,grp,gzip,hashlib,heapq,hmac,html,http,imaplib,imghdr,imp,importlib,inspect,io,ipaddress,itertools,json,keyword,lib2to3,linecache,locale,logging,lzma,macpath,mailbox,mailcap,marshal,math,mimetypes,mmap,modulefinder,msilib,msvcrt,multiprocessing,netrc,nis,nntplib,ntpath,numbers,operator,optparse,os,ossaudiodev,parser,pathlib,pdb,pickle,pickletools,pipes,pkgutil,platform,plistlib,poplib,posix,posixpath,pprint,profile,p stats,pty,pwd,py_compile,pyclbr,pydoc,queue,quopri,random,re,readline,reprlib,resource,rlcompleter,runpy,sched,secrets,select,selectors,shelve,shlex,shutil,signal,site,smtpd,smtplib,sndhdr,socket,socketserver,spwd,sqlite3,sre,sre_compile,sre_constants,sre_parse,ssl,stat,statistics,string,stringprep,struct,subprocess,sunau,symbol,symtable,sys,sysconfig,syslog,tabnanny,tarfile,telnetlib,tempfile,termios,test,textwrap,threading,time,timeit,tkinter,token,tokenize,trace,traceback,tracemalloc,tty,turtle,turtledemo,types,typing,unicodedata,unittest,urllib,uu,uuid,venv,warnings,wave,weakref,webbrowser,winreg,winsound,wsgiref,xdrlib,xml,xmlrpc,zipapp,zipfile,zipimport,zlib diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.2.2/setup.py new/Radicale-3.2.3/setup.py --- old/Radicale-3.2.2/setup.py 2024-06-18 20:24:12.000000000 +0200 +++ new/Radicale-3.2.3/setup.py 2024-08-30 06:19:51.000000000 +0200 @@ -19,7 +19,7 @@ # When the version is updated, a new section in the CHANGELOG.md file must be # added too. -VERSION = "3.2.2" +VERSION = "3.2.3" with open("README.md", encoding="utf-8") as f: long_description = f.read() @@ -38,9 +38,9 @@ install_requires = ["defusedxml", "passlib", "vobject>=0.9.6", "python-dateutil>=2.7.3", "pika>=1.1.0", - "setuptools; python_version<'3.9'"] + ] bcrypt_requires = ["bcrypt"] -test_requires = ["pytest>=7", "typeguard<4.3", "waitress", *bcrypt_requires] +test_requires = ["pytest>=7", "waitress", *bcrypt_requires] setup( name="Radicale", @@ -75,6 +75,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Office/Business :: Groupware"])
