Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package Radicale for openSUSE:Factory checked in at 2026-08-29 17:43:43 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/Radicale (Old) and /work/SRC/openSUSE:Factory/.Radicale.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "Radicale" Sat Aug 29 17:43:43 2026 rev:32 rq:1374593 version:3.7.8 Changes: -------- --- /work/SRC/openSUSE:Factory/Radicale/Radicale.changes 2026-08-04 22:48:31.022753200 +0200 +++ /work/SRC/openSUSE:Factory/.Radicale.new.1265/Radicale.changes 2026-08-29 17:44:47.842213746 +0200 @@ -1,0 +2,15 @@ +Sat Aug 22 08:58:49 UTC 2026 - Ákos Szőts <[email protected]> + +- Warning: upcoming 3.8.0 will be enhanced with sharing-by-group/realm feature, internal technical limitations leads to required enhanced username syntax check, following (unusual) usernames will be no longer supported: + * username starts or ends with @ + * username containing more than one @ + * username containing : +- Update to 3.7.8 + * Fix: time-range filter on a VTODO having DTSTART/DUE and also CREATED/COMPLETED used the CREATED->COMPLETED duration instead of the DTSTART->DUE one, so completed tasks were missing from (or wrongly returned by) calendar-query REPORT results + * Fix: sharing/proppatch: reject in case of write-access but 'p' is in permissions + * Fix: sharing/by-map: catch collection path without trailing / (supporting "pimsync") + * Improvements + * Add: [report] max_expand_occurrence option to separate from max_freebusy_occurrence + * Add: [system] max_vevent_rrule_occurrence option to catch DoS by problematic RRULE early enough (workaround for missing protection in current vobject version) + +------------------------------------------------------------------- Old: ---- Radicale-3.7.7.tar.gz New: ---- Radicale-3.7.8.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ Radicale.spec ++++++ --- /var/tmp/diff_new_pack.XIxU7S/_old 2026-08-29 17:44:48.610240912 +0200 +++ /var/tmp/diff_new_pack.XIxU7S/_new 2026-08-29 17:44:48.611240948 +0200 @@ -28,7 +28,7 @@ %define py_min_ver 3.9 %define vo_min_ver 0.9.6 Name: Radicale -Version: 3.7.7 +Version: 3.7.8 Release: 0 Summary: A CalDAV calendar and CardDav contact server License: GPL-3.0-or-later ++++++ Radicale-3.7.7.tar.gz -> Radicale-3.7.8.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/CHANGELOG.md new/Radicale-3.7.8/CHANGELOG.md --- old/Radicale-3.7.7/CHANGELOG.md 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/CHANGELOG.md 2026-08-06 08:01:32.000000000 +0200 @@ -1,5 +1,12 @@ # Changelog +## 3.7.8 +* Fix: time-range filter on a VTODO having DTSTART/DUE and also CREATED/COMPLETED used the CREATED->COMPLETED duration instead of the DTSTART->DUE one, so completed tasks were missing from (or wrongly returned by) calendar-query REPORT results +* Fix: sharing/proppatch: reject in case of write-access but 'p' is in permissions +* Fix: sharing/by-map: catch collection path without trailing / (supporting "pimsync") +* Add: [report] max_expand_occurrence option to separate from max_freebusy_occurrence +* Add: [system] max_vevent_rrule_occurrence option to catch DoS by problematic RRULE early enough (workaround for missing protection in current vobject version) + ## 3.7.7 * Fix: web plugin helpers httputils.serve_resource/serve_folder ignored their mimetypes and fallback_mimetype parameters and always used the built-in mapping, so custom web plugins could not serve additional file types with a correct Content-Type * Fix: free-busy REPORT always failed with HTTP 400 ("FREEBUSY occurrences limit of 0 hit") when [reporting] max_freebusy_occurrence is set to 0 (limit disabled), because the limit check did not honor the disabled limit @@ -8,7 +15,6 @@ * Fix: text-match filter on a structured property (e.g. vCard N or ADR) crashed with HTTP 500 (AttributeError: 'Name'/'Address' object has no attribute 'lower') because vobject parses these into non-string objects; their text representation is now used * Fix: sharing bday-to-ICS conversion assigned the empty-FN fallback marker to the {nickname} placeholder instead of {fn}, so a VCARD with an empty FN got its NICKNAME overwritten with "!fn!" in the generated SUMMARY/DESCRIPTION and the {fn} fallback never resolved * Fix: sharing/delete: add forgotten backmap -* Fix: sharing/proppatch: reject in case of write-access but 'p' is in permissions ## 3.7.6 * Extension: item verification on commandline diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/DOCUMENTATION.md new/Radicale-3.7.8/DOCUMENTATION.md --- old/Radicale-3.7.7/DOCUMENTATION.md 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/DOCUMENTATION.md 2026-08-06 08:01:32.000000000 +0200 @@ -866,6 +866,19 @@ Announced to clients requesting "max-resource-size" via PROPFIND. +##### max_vevent_rrule_occurrence + +_(>= 3.7.8)_ + +The maximum of occurrence by an rrule of a vevent. +Large time frames defined in RRULE by COUNT or UNTIL 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 accepting the item. + +Default: `10000` + ##### timeout Socket timeout. (seconds) @@ -2191,6 +2204,19 @@ #### [reporting] +##### max_expand_occurrence + +_(>= 3.7.8)_ + +When returning an expanded report, a list of 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 + ##### max_freebusy_occurrence _(>= 3.2.3)_ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/config new/Radicale-3.7.8/config --- old/Radicale-3.7.7/config 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/config 2026-08-06 08:01:32.000000000 +0200 @@ -33,6 +33,9 @@ # Announced to clients requesting "max-resource-size" via PROPFIND #max_resource_size = 10000000 +# Max occurrence by an RRULE, limit the number to prevent DoS attacks. +#max_vevent_rrule_occurrence = 10000 + # Socket timeout (seconds) #timeout = 30 @@ -499,6 +502,10 @@ [reporting] +# When returning an expanded report, limit the number of returned +# occurences per event to prevent DoS attacks. +#max_expand_occurrence = 10000 + # 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.7.7/pyproject.toml new/Radicale-3.7.8/pyproject.toml --- old/Radicale-3.7.7/pyproject.toml 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/pyproject.toml 2026-08-06 08:01:32.000000000 +0200 @@ -3,7 +3,7 @@ # When the version is updated, a new section in the CHANGELOG.md file must be # added too. readme = "README.md" -version = "3.7.7" +version = "3.7.8" authors = [{name = "Guillaume Ayoub", email = "[email protected]"}, {name = "Unrud", email = "[email protected]"}, {name = "Peter Bieringer", email = "[email protected]"}] license = {text = "GNU GPL v3"} description = "CalDAV and CardDAV Server" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/__main__.py new/Radicale-3.7.8/radicale/__main__.py --- old/Radicale-3.7.7/radicale/__main__.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/__main__.py 2026-08-06 08:01:32.000000000 +0200 @@ -200,9 +200,10 @@ if args_ns.verify_item: encoding = configuration.get("encoding", "stock") + max_vevent_rrule_occurrence = configuration.get("server", "max_vevent_rrule_occurrence") logger.info("Item verification start using 'stock' encoding: %s", encoding) try: - if not item.verify(args_ns.verify_item[0], encoding): + if not item.verify(args_ns.verify_item[0], encoding, max_vevent_rrule_occurrence): logger.critical("Item verification failed") sys.exit(1) except Exception as e: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/__init__.py new/Radicale-3.7.8/radicale/app/__init__.py --- old/Radicale-3.7.7/radicale/app/__init__.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/app/__init__.py 2026-08-06 08:01:32.000000000 +0200 @@ -79,6 +79,7 @@ _internal_server: bool _max_content_length: int _max_resource_size: int + _max_vevent_rrule_occurrence: int _auth_realm: str _auth_type: str _web_type: str @@ -120,6 +121,8 @@ self._max_resource_size = max_resource_size_limited else: logger.info("max_resource_size set to: %d bytes (%sbytes)", self._max_resource_size, utils.format_unit(self._max_resource_size, binary=True)) + self._max_vevent_rrule_occurrence = configuration.get("server", "max_vevent_rrule_occurrence") + logger.info("max_vevent_rrule_occurrence set to: %d", self._max_vevent_rrule_occurrence) self._bad_put_request_content = configuration.get("logging", "bad_put_request_content") logger.info("log bad put request content: %s", self._bad_put_request_content) self._request_header_on_debug = configuration.get("logging", "request_header_on_debug") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/base.py new/Radicale-3.7.8/radicale/app/base.py --- old/Radicale-3.7.7/radicale/app/base.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/app/base.py 2026-08-06 08:01:32.000000000 +0200 @@ -122,6 +122,7 @@ _sharing: sharing.BaseSharing _encoding: str _max_resource_size: int + _max_vevent_rrule_occurrence: int _permit_delete_collection: bool _permit_overwrite_collection: bool _strict_preconditions: bool diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/proppatch.py new/Radicale-3.7.8/radicale/app/proppatch.py --- old/Radicale-3.7.7/radicale/app/proppatch.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/app/proppatch.py 2026-08-06 08:01:32.000000000 +0200 @@ -142,6 +142,12 @@ else: logger.trace("PROPPATCH/xml_proppatch: write-access: %r", path) if share: + if "p" in share['Permissions']: + logger.notice("PROPPATCH request on shared %r: write-permissions, but disabled by share permission 'p'", path_orig) + return httputils.NOT_ALLOWED + elif "p" in raw_permissions: + logger.notice("PROPPATCH request on shared %r: write-permissions, but disabled by rights permission 'p'", path_orig) + return httputils.NOT_ALLOWED # write access -> check for enforced properties overlay logger.trace("PROPPATCH/xml_proppatch: write-access/sharing: %r", path_orig) if self._sharing.enforce_properties_overlay: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/put.py new/Radicale-3.7.8/radicale/app/put.py --- old/Radicale-3.7.7/radicale/app/put.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/app/put.py 2026-08-06 08:01:32.000000000 +0200 @@ -47,6 +47,7 @@ def prepare(vobject_items: List[vobject.base.Component], path: str, content_type: str, permission: bool, parent_permission: bool, max_resource_size: int, + max_vevent_rrule_occurrence: int, tag: Optional[str] = None, write_whole_collection: Optional[bool] = None) -> Tuple[ Iterator[radicale_item.Item], # items @@ -73,7 +74,9 @@ try: if tag and write_whole_collection is not None: radicale_item.check_and_sanitize_items( - vobject_items, is_collection=write_whole_collection, tag=tag) + vobject_items, + max_vevent_rrule_occurrence=max_vevent_rrule_occurrence, + is_collection=write_whole_collection, tag=tag) if write_whole_collection and tag == "VCALENDAR": vobject_components: List[vobject.base.Component] = [] vobject_item, = vobject_items @@ -224,7 +227,9 @@ vobject_items, path, content_type, bool(rights.intersect(access.permissions, "Ww")), bool(rights.intersect(access.parent_permissions, "w")), - self._max_resource_size) + self._max_resource_size, + self._max_vevent_rrule_occurrence, + ) with self._storage.acquire_lock("w", user, path=path, request="PUT"): item = next(iter(self._storage.discover(path)), None) @@ -289,6 +294,7 @@ bool(rights.intersect(access.permissions, "Ww")), bool(rights.intersect(access.parent_permissions, "w")), self._max_resource_size, + self._max_vevent_rrule_occurrence, tag, write_whole_collection) props = prepared_props if prepared_exc_info: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/report.py new/Radicale-3.7.8/radicale/app/report.py --- old/Radicale-3.7.7/radicale/app/report.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/app/report.py 2026-08-06 08:01:32.000000000 +0200 @@ -895,9 +895,9 @@ assert item.collection is not None collection = item.collection - max_occurrence = self.configuration.get("reporting", "max_freebusy_occurrence") 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, @@ -909,6 +909,7 @@ headers = {"Content-Type": "text/calendar; charset=%s" % self._encoding} return status, headers, str(body), xmlutils.pretty_xml(xml_content) else: + max_occurrence = self.configuration.get("reporting", "max_expand_occurrence") try: status, xml_answer = xml_report( base_prefix, path, xml_content, collection, self._encoding, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/config.py new/Radicale-3.7.8/radicale/config.py --- old/Radicale-3.7.7/radicale/config.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/config.py 2026-08-06 08:01:32.000000000 +0200 @@ -254,6 +254,10 @@ "value": "10000000", "help": "maximum size of resource (default: 10 Mbyte)", "type": positive_int}), + ("max_vevent_rrule_occurrence", { + "value": "10000", + "help": "maximum occurrence by an RRULE (default: 10000)", + "type": positive_int}), ("timeout", { "value": "30", "help": "socket timeout", @@ -813,9 +817,13 @@ ("headers", OrderedDict([ ("_allow_extra", str)])), ("reporting", OrderedDict([ + ("max_expand_occurrence", { + "value": "10000", + "help": "number of expand occurrences per event when reporting", + "type": positive_int}), ("max_freebusy_occurrence", { "value": "10000", - "help": "number of occurrences per event when reporting", + "help": "number of free-busy occurrences per event when reporting", "type": positive_int})])) ]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/item/__init__.py new/Radicale-3.7.8/radicale/item/__init__.py --- old/Radicale-3.7.7/radicale/item/__init__.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/item/__init__.py 2026-08-06 08:01:32.000000000 +0200 @@ -53,6 +53,17 @@ # List of BDAY years acting as flag for "no year specified" VCF_TO_ICS_BDAY_NO_YEAR: list = ["1604"] +# List of RRULE frequencies and their interval in seconds +RRULE_FREQUENCIES_TO_INTERVAL: dict[str, float] = { + "YEARLY": 60*60*24*365, + "MONTHLY": 60*60*24*365/12, + "WEEKLY": 60*60*24*7, + "DAILY": 60*60*24, + "HOURLY": 60*60, + "MINUTELY": 60, + "SECONDLY": 1, + } + def read_components(s: str) -> List[vobject.base.Component]: """Wrapper for vobject.readComponents""" @@ -103,6 +114,7 @@ def check_and_sanitize_items( vobject_items: List[vobject.base.Component], + max_vevent_rrule_occurrence: int, is_collection: bool = False, tag: str = "") -> None: """Check vobject items for common errors and add missing UIDs. @@ -232,11 +244,77 @@ if ref_value_param is not None: dates.params["VALUE"] = ref_value_param # vobject interprets recurrence rules on demand - try: - component.rruleset - except Exception as e: - raise ValueError("Invalid recurrence rules in %s in object %r" - % (component.name, component_uid)) from e + if hasattr(component, "rrule"): + # workaround for vobject < 1.0.0 as it has no limiter in "getrruleset" + logger.trace("Recurrence rule found in %s in object %r: %r", component.name, component_uid, component.rrule.value) + if not hasattr(component, "dtstart"): + # e.g. VTODO + rrule = vobject.icalendar.rrule.rrulestr(component.rrule.value) + else: + dtstart = radicale_filter.date_to_datetime(component.dtstart.value) + ignoretz = ( + not isinstance(dtstart, datetime.datetime) + or dtstart.tzinfo is None + ) + rrule = vobject.icalendar.rrule.rrulestr(component.rrule.value, ignoretz=ignoretz) + # early check of maximum of COUNT to avoid DoS (workaround) + if hasattr(rrule, "_count") and rrule._count is not None: + logger.trace("Recurrence rule %r in %s in object %r contains: COUNT=%d", component.rrule.value, component.name, component_uid, rrule._count) + if max_vevent_rrule_occurrence > 0 and rrule._count > max_vevent_rrule_occurrence: + logger.error("Recurrence rule %r count in %s in object %r: %d (REJECTED/limit: %d)", component.rrule.value, component.name, component_uid, rrule._count, max_vevent_rrule_occurrence) + raise ValueError("Too many recurrence rule entries in %s in object %r: %d (limit: %d)" + % (component.name, component_uid, rrule._count, max_vevent_rrule_occurrence)) + else: + logger.debug("Recurrence rule count in %s in object %r: %d (PASSED/limit: %d)" % (component.name, component_uid, rrule._count, max_vevent_rrule_occurrence)) + else: + logger.trace("Recurrence rule %r in %s in object %r doesn't contain: COUNT", component.rrule.value, component.name, component_uid) + # early check of maximum of (UNTIL-DTSTART)/interval(FREQ) to avoid DoS (ugly workaround with some guessing) + if hasattr(rrule, "_freq") and rrule._freq is not None and hasattr(rrule, "_until") and rrule._until is not None and hasattr(component, "dtstart"): + if vobject.icalendar.FREQUENCIES[rrule._freq] not in RRULE_FREQUENCIES_TO_INTERVAL: + raise ValueError("Unsupported FREQ in recurrence rule in %s in object %r: %r" + % (component.name, component_uid, rrule._freq)) + # RRULE has known FREQ+UNTIL+DTSTART + # TZ code taken from vobject/icalendar.py/getrruleset + if rrule._until.tzinfo is None: + rrule._until = rrule._until.replace(tzinfo=dtstart.tzinfo) + if dtstart.tzinfo is not None: + rrule._until = rrule._until.astimezone(dtstart.tzinfo) + delta = rrule._until - dtstart + seconds = delta.total_seconds() + if seconds < 0: + # UNTIL < DTSTART + logger.error("Recurrence rule %r in %s in object %r REJECTED, UNTIL < DTSTART", component.rrule.value, component.name, component_uid) + raise ValueError("Recurrence rule in %s in object %r has UNTIL < DTSTART" + % (component.name, component_uid)) + rrule_entries = seconds / RRULE_FREQUENCIES_TO_INTERVAL[vobject.icalendar.FREQUENCIES[rrule._freq]] + if max_vevent_rrule_occurrence > 0 and rrule_entries > max_vevent_rrule_occurrence: + logger.warning("Recurrence rule %r entries in %s in object %r: %d (estimated/REJECTED/limit: %d)", component.rrule.value, component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence) + raise ValueError("Too many recurrence rule entries in %s in object %r: %d (limit: %d)" + % (component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence)) + else: + logger.debug("Recurrence rule %r entries in %s in object %r: %d (estimated/PASSED/limit: %d)" % (component.rrule.value, component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence)) + else: + logger.trace("Recurrence rule %r in %s in object %r doesn't contain: FREQ+UNTIL", component.rrule.value, component.name, component_uid) + # generic check by vobject + try: + rruleset = component.rruleset + except Exception as e: + raise ValueError("Invalid recurrence rules in %s in object %r" + % (component.name, component_uid)) from e + # check limit (last resort) + infinite = False + if (";UNTIL=" not in component.rrule.value and + ";COUNT=" not in component.rrule.value): + infinite = True + + if infinite is False: + rrule_entries = len(list(rruleset)) + if max_vevent_rrule_occurrence > 0 and rrule_entries > max_vevent_rrule_occurrence: + logger.warning("Recurrence rule %r entries in %s in object %r: %d (calculated/REJECTED/limit: %d)", component.rrule.value, component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence) + raise ValueError("Too many recurrence rule entries in %s in object %r: %d (limit: %d)" + % (component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence)) + else: + logger.debug("Recurrence rule %r entries in %s in object %r: %d (calculated/PASSED/limit: %d)", component.rrule.value, component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence) elif tag == "VADDRESSBOOK": # https://tools.ietf.org/html/rfc6352#section-5.1 object_uids = set() @@ -387,7 +465,7 @@ return math.floor(start.timestamp()), math.ceil(end.timestamp()) -def verify(file: str, encoding: str): +def verify(file: str, encoding: str, max_vevent_rrule_occurrence: int): logger.info("Verifying item: %s", file) with open(file, "rb") as f: content_raw = f.read() @@ -407,7 +485,7 @@ try: tag = radicale_item.predict_tag_of_whole_collection(vobject_items) if tag is not None: - radicale_item.check_and_sanitize_items(vobject_items, tag=tag) + radicale_item.check_and_sanitize_items(vobject_items, tag=tag, max_vevent_rrule_occurrence=max_vevent_rrule_occurrence) else: raise ValueError("collection tag cannot be predicted") except Exception as e: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/item/filter.py new/Radicale-3.7.8/radicale/item/filter.py --- old/Radicale-3.7.7/radicale/item/filter.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/item/filter.py 2026-08-06 08:01:32.000000000 +0200 @@ -442,7 +442,10 @@ completed = date_to_datetime(completed.value) if created is not None: created = date_to_datetime(created.value) - original_duration = (completed - created).total_seconds() + # NOTE: kept separate from "original_duration", otherwise a + # VTODO with DTSTART+DUE and also CREATED+COMPLETED would + # lose its DTSTART->DUE duration (see line 2 below) + completed_duration = (completed - created).total_seconds() elif created is not None: created = date_to_datetime(created.value) @@ -500,7 +503,7 @@ elif completed is not None and created is not None: # Line 5 completed = reference_date + timedelta( - seconds=original_duration) + seconds=completed_duration) if (range_fn(reference_date - SECOND, reference_date + SECOND, is_recurrence) or diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/sharing/__init__.py new/Radicale-3.7.8/radicale/sharing/__init__.py --- old/Radicale-3.7.7/radicale/sharing/__init__.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/sharing/__init__.py 2026-08-06 08:01:32.000000000 +0200 @@ -628,6 +628,7 @@ """ returning dict with PathMapped, Owner, Permissions or None if invalid""" if self.sharing_collection_by_map: logger.trace("sharing/map/resolver: check path: %r", path) + # check collection path result = self.database_get_sharing( ShareType="map", PathOrToken=path, @@ -635,7 +636,7 @@ User=user) if not result: - # fallback to parent path + # assume item path, fallback to parent path parent_path = pathutils.parent_path(path) logger.trace("sharing/map/resolver: check parent path: %r", parent_path) result = self.database_get_sharing( @@ -646,9 +647,19 @@ if result: result['PathMapped'] = path.replace(parent_path, result['PathMapped']) logger.trace("sharing/map/resolver: PathMapped=%r Permissions=%r by parent_path=%r", result['PathMapped'], result['Permissions'], parent_path) - else: - logger.trace("sharing/map/resolver: not found") - return None + + if not result and not path.endswith("/"): + # assume collection path with missing trailing / + path_with_trailing_slash = path + "/" + logger.trace("sharing/map/resolver: check path having '/' appended: %r", path_with_trailing_slash) + result = self.database_get_sharing( + ShareType="map", + PathOrToken=path_with_trailing_slash, + OnlyEnabled=False, + User=user) + if result: + path = path_with_trailing_slash + logger.trace("sharing/map/resolver: PathMapped=%r Permissions=%r by path_with_trailing_slash=%r", result['PathMapped'], result['Permissions'], path_with_trailing_slash) if result: if result['EnabledByOwner'] is not True: @@ -666,6 +677,7 @@ logger.info("sharing/%s: resolved path %r->%r, user %r->%r, Permissions=%r Conversion=%r", "map", result['PathOrToken'], result['PathMapped'], user, result['Owner'], result['Permissions'], result['Conversion']) return result + logger.trace("sharing/map/resolver: not found") return None else: logger.trace("sharing/map: not active") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/storage/multifilesystem/base.py new/Radicale-3.7.8/radicale/storage/multifilesystem/base.py --- old/Radicale-3.7.7/radicale/storage/multifilesystem/base.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/storage/multifilesystem/base.py 2026-08-06 08:01:32.000000000 +0200 @@ -106,6 +106,7 @@ "logging", "storage_cache_actions_on_debug") self._max_resource_size = configuration.get( "server", "max_resource_size") + self._max_vevent_rrule_occurrence = configuration.get("server", "max_vevent_rrule_occurrence") def _get_collection_root_folder(self) -> str: return os.path.join(self._filesystem_folder, "collection-root") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/storage/multifilesystem/get.py new/Radicale-3.7.8/radicale/storage/multifilesystem/get.py --- old/Radicale-3.7.7/radicale/storage/multifilesystem/get.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/storage/multifilesystem/get.py 2026-08-06 08:01:32.000000000 +0200 @@ -124,7 +124,7 @@ vobject_items = radicale_item.read_components( raw_text.decode(self._encoding)) radicale_item.check_and_sanitize_items( - vobject_items, tag=self.tag) + vobject_items, tag=self.tag, max_vevent_rrule_occurrence=self._storage._max_vevent_rrule_occurrence) vobject_item, = vobject_items temp_item = radicale_item.Item( collection=self, vobject_item=vobject_item) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/static/event1_y2040.ics new/Radicale-3.7.8/radicale/tests/static/event1_y2040.ics --- old/Radicale-3.7.7/radicale/tests/static/event1_y2040.ics 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.7.8/radicale/tests/static/event1_y2040.ics 2026-08-06 08:01:32.000000000 +0200 @@ -0,0 +1,35 @@ +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:event1-Y2040 +SUMMARY:Event in year 2040 +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:20400901T180000 +DTEND;TZID=Europe/Paris:20400901T190000 +END:VEVENT +END:VCALENDAR diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_count_500.ics new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_count_500.ics --- old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_count_500.ics 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_count_500.ics 2026-08-06 08:01:32.000000000 +0200 @@ -0,0 +1,31 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=US/Eastern:20060102 +DTEND;TZID=US/Eastern:20060103 +RRULE:FREQ=DAILY;COUNT=500 +SUMMARY:Recurring event with count 500 +UID:event_full_day_rrule_count_500 +DTSTAMP:20060102T094829Z +END:VEVENT +END:VCALENDAR + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_2y.ics new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_2y.ics --- old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_2y.ics 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_2y.ics 2026-08-06 08:01:32.000000000 +0200 @@ -0,0 +1,31 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=US/Eastern:20060102 +DTEND;TZID=US/Eastern:20060103 +RRULE:FREQ=DAILY;UNTIL=20080101 +SUMMARY:Recurring event with until +2y +UID:event_full_day_rrule_until_2y +DTSTAMP:20060102T094829Z +END:VEVENT +END:VCALENDAR + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_5000y.ics new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_5000y.ics --- old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_5000y.ics 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_5000y.ics 2026-08-06 08:01:32.000000000 +0200 @@ -0,0 +1,31 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=US/Eastern:20060102 +DTEND;TZID=US/Eastern:20060103 +RRULE:FREQ=DAILY;UNTIL=70060101 +SUMMARY:Recurring event with until +5000y +UID:event_full_day_rrule_until_5000y +DTSTAMP:20060102T094829Z +END:VEVENT +END:VCALENDAR + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_50y.ics new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_50y.ics --- old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_50y.ics 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_50y.ics 2026-08-06 08:01:32.000000000 +0200 @@ -0,0 +1,31 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=US/Eastern:20060102 +DTEND;TZID=US/Eastern:20060103 +RRULE:FREQ=DAILY;UNTIL=20560101 +SUMMARY:Recurring event with until +50y +UID:event_full_day_rrule_until_50y +DTSTAMP:20060102T094829Z +END:VEVENT +END:VCALENDAR + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_before_dtstart.ics new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_before_dtstart.ics --- old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_before_dtstart.ics 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_before_dtstart.ics 2026-08-06 08:01:32.000000000 +0200 @@ -0,0 +1,31 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +BEGIN:VTIMEZONE +LAST-MODIFIED:20040110T032845Z +TZID:US/Eastern +BEGIN:DAYLIGHT +DTSTART:20000404T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZNAME:EDT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20001026T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=US/Eastern:20060102 +DTEND;TZID=US/Eastern:20060103 +RRULE:FREQ=DAILY;UNTIL=20050101 +SUMMARY:Recurring event with until in the past +UID:event_full_day_rrule_until_in_the_past +DTSTAMP:20060102T094829Z +END:VEVENT +END:VCALENDAR + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/static/todo10.ics new/Radicale-3.7.8/radicale/tests/static/todo10.ics --- old/Radicale-3.7.7/radicale/tests/static/todo10.ics 1970-01-01 01:00:00.000000000 +0100 +++ new/Radicale-3.7.8/radicale/tests/static/todo10.ics 2026-08-06 08:01:32.000000000 +0200 @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Radicale//NONSGML Radicale Server//EN +BEGIN:VTODO +UID:todo10 +SUMMARY:todo10 +DTSTART:20130901T180000Z +DUE:20130903T180000Z +CREATED:20130901T170000Z +COMPLETED:20130901T173000Z +STATUS:COMPLETED +END:VTODO +END:VCALENDAR diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/test_base.py new/Radicale-3.7.8/radicale/tests/test_base.py --- old/Radicale-3.7.7/radicale/tests/test_base.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/tests/test_base.py 2026-08-06 08:01:32.000000000 +0200 @@ -151,6 +151,19 @@ assert "Event" in answer assert "UID:event" in answer + def test_add_event_y2040(self) -> None: + """Add an event with year 2040.""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event1_y2040.ics") + path = "/calendar.ics/event1_y2040.ics" + self.put(path, event) + _, headers, answer = self.request("GET", path, check=200) + assert "ETag" in headers + assert headers["Content-Type"] == "text/calendar; charset=utf-8" + assert "VEVENT" in answer + assert "Event" in answer + assert "UID:event" in answer + @pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject <= 0.9.9 does not support PERIOD") def test_add_event_with_rdate_period_start_duration_single(self) -> None: """Add an event with RDATE/PERIOD with start+duration.""" @@ -328,6 +341,72 @@ event = get_file_content("event_mixed_datetime_and_date.ics") self.put("/calendar.ics/event.ics", event) + def test_add_event_with_rrule_count_500_limit_100(self) -> None: + """Test event with RRULE COUNT=500 and limit 100.""" + self.configure({"server": {"max_vevent_rrule_occurrence": 100}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_full_day_rrule_count_500.ics") + self.put("/calendar.ics/event_full_day_rrule_count_500.ics", event, check=400) + + def test_add_event_with_rrule_count_500_limit_600(self) -> None: + """Test event with RRULE COUNT=500 and limit 600.""" + self.configure({"server": {"max_vevent_rrule_occurrence": 600}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_full_day_rrule_count_500.ics") + self.put("/calendar.ics/event_full_day_rrule_count_500.ics", event) + + def test_add_event_with_rrule_until_2y_limit_100(self) -> None: + """Test event with RRULE UNTIL=+2y and limit 100.""" + self.configure({"server": {"max_vevent_rrule_occurrence": 100}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_full_day_rrule_until_2y.ics") + self.put("/calendar.ics/event_full_day_rrule_until_2y.ics", event, check=400) + + def test_add_event_with_rrule_until_2y_limit_800(self) -> None: + """Test event with RRULE UNTIL=+2y and limit 800.""" + self.configure({"server": {"max_vevent_rrule_occurrence": 800}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_full_day_rrule_until_2y.ics") + self.put("/calendar.ics/event_full_day_rrule_until_2y.ics", event) + + def test_add_event_with_rrule_until_before_dtstart(self) -> None: + """Test event with RRULE UNTIL < DTSTART.""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_full_day_rrule_until_before_dtstart.ics") + self.put("/calendar.ics/event_full_day_rrule_until_before_dtstart.ics", event, check=400) + + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 32-bit platform") + def test_add_event_with_rrule_until_50y_limit_100(self) -> None: + """Test event with RRULE UNTIL=+50y and limit 100.""" + self.configure({"server": {"max_vevent_rrule_occurrence": 100}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_full_day_rrule_until_50y.ics") + self.put("/calendar.ics/event_full_day_rrule_until_50y.ics", event, check=400) + + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 32-bit platform") + def test_add_event_with_rrule_until_50y_limit_20000(self) -> None: + """Test event with RRULE UNTIL=+50y and limit 20000.""" + self.configure({"server": {"max_vevent_rrule_occurrence": 20000}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_full_day_rrule_until_50y.ics") + self.put("/calendar.ics/event_full_day_rrule_until_50y.ics", event) + + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 32-bit platform") + def test_add_event_with_rrule_until_5000y_limit_100(self) -> None: + """Test event with RRULE UNTIL=+5000y and limit 100.""" + self.configure({"server": {"max_vevent_rrule_occurrence": 100}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_full_day_rrule_until_5000y.ics") + self.put("/calendar.ics/event_full_day_rrule_until_5000y.ics", event, check=400) + + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 32-bit platform") + def test_add_event_with_rrule_until_5000y_limit_2000000(self) -> None: + """Test event with RRULE UNTIL=+5000y and limit 2000000.""" + self.configure({"server": {"max_vevent_rrule_occurrence": 2000000}}) + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_full_day_rrule_until_5000y.ics") + self.put("/calendar.ics/event_full_day_rrule_until_5000y.ics", event) + def test_add_event_with_exdate_without_rrule(self) -> None: """Test event with EXDATE but not having RRULE.""" self.mkcalendar("/calendar.ics/") @@ -2004,6 +2083,27 @@ </C:comp-filter>"""], "todo", items=range(1, 9)) assert "/calendar.ics/todo7.ics" in answer + def test_time_range_filter_todos_dtstart_due_completed(self) -> None: + """Report request with time-range filter on a completed todo which + also has DTSTART and DUE (rfc4791-9.9: DTSTART/DUE take + precedence over CREATED/COMPLETED).""" + # inside DTSTART..DUE, but outside CREATED..COMPLETED + answer = self._test_filter(["""\ +<C:comp-filter name="VCALENDAR"> + <C:comp-filter name="VTODO"> + <C:time-range start="20130902T000000Z" end="20130903T000000Z"/> + </C:comp-filter> +</C:comp-filter>"""], "todo", items=(10,)) + assert "/calendar.ics/todo10.ics" in answer + # entirely before DTSTART and CREATED + answer = self._test_filter(["""\ +<C:comp-filter name="VCALENDAR"> + <C:comp-filter name="VTODO"> + <C:time-range start="20130801T000000Z" end="20130901T000000Z"/> + </C:comp-filter> +</C:comp-filter>"""], "todo", items=(10,)) + assert "/calendar.ics/todo10.ics" not in answer + def test_time_range_filter_events_valarm(self) -> None: """Report request with time-range filter on events having absolute VALARM.""" answer = self._test_filter(["""\ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/test_expand.py new/Radicale-3.7.8/radicale/tests/test_expand.py --- old/Radicale-3.7.7/radicale/tests/test_expand.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/tests/test_expand.py 2026-08-06 08:01:32.000000000 +0200 @@ -128,7 +128,8 @@ expected_start_times: List[str], expected_end_times: List[str], only_dates: bool, - nr_uids: int) -> None: + nr_uids: int, + check: int = 207) -> None: _, responses = self.report("/calendar.ics/", self._req_without_expand(expected_uid, start, end)) assert len(responses) == 1 @@ -154,7 +155,9 @@ assert len(uids) == nr_uids _, responses = self.report("/calendar.ics/", - self._req_with_expand(expected_uid, start, end)) + self._req_with_expand(expected_uid, start, end), check=check) + if check != 207: + return assert len(responses) == 1 @@ -176,17 +179,21 @@ uids.append(line) if line.startswith("RECURRENCE-ID:"): - assert line in expected_recurrence_ids + if expected_recurrence_ids: + assert line in expected_recurrence_ids recurrence_ids.append(line) if line.startswith("DTSTART:"): - assert line in expected_start_times + if expected_start_times: + assert line in expected_start_times if line.startswith("DTEND:"): - assert line in expected_end_times + if expected_end_times: + assert line in expected_end_times - assert len(uids) == len(expected_recurrence_ids) - assert len(set(recurrence_ids)) == len(expected_recurrence_ids) + if expected_recurrence_ids: + assert len(uids) == len(expected_recurrence_ids) + assert len(set(recurrence_ids)) == len(expected_recurrence_ids) def _test_expand_max(self, expected_uid: str, @@ -289,6 +296,35 @@ 1 ) + def test_report_with_expand_property_all_day_count_500_event_pass(self) -> None: + """Test report with expand property for all day count 500 events""" + self.configure({"reporting": {"max_expand_occurrence": 501}}) + self._test_expand( + "event_full_day_rrule_count_500", + "20060103T000000Z", + "20080105T000000Z", + [], + [], + [], + ONLY_DATES, + 1 + ) + + def test_report_with_expand_property_all_day_count_500_event_reject(self) -> None: + """Test report with expand property for all day count 500 events""" + self.configure({"reporting": {"max_expand_occurrence": 10}}) + self._test_expand( + "event_full_day_rrule_count_500", + "20060103T000000Z", + "20080105T000000Z", + [], + [], + [], + ONLY_DATES, + 1, + 400 + ) + def test_report_with_expand_property_overridden(self) -> None: """Test report with expand property with overridden events""" self._test_expand( @@ -326,7 +362,7 @@ def test_report_with_expand_property_max_occur(self) -> None: """Test report with expand property too many vevents""" - self.configure({"reporting": {"max_freebusy_occurrence": 100}}) + self.configure({"reporting": {"max_expand_occurrence": 100}}) self._test_expand_max( "event_daily_rrule_forever", "20060103T000000Z", @@ -336,7 +372,7 @@ def test_report_with_max_occur(self) -> None: """Test report with too many vevents""" - self.configure({"reporting": {"max_freebusy_occurrence": 10}}) + self.configure({"reporting": {"max_expand_occurrence": 10}}) uid = "event_multiple_too_many" start = "20130901T000000Z" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/test_sharing.py new/Radicale-3.7.8/radicale/tests/test_sharing.py --- old/Radicale-3.7.7/radicale/tests/test_sharing.py 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/radicale/tests/test_sharing.py 2026-08-06 08:01:32.000000000 +0200 @@ -1988,12 +1988,36 @@ json_dict['PathOrToken'] = path_shared _, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user:userpw", json_dict=json_dict) + # check GET as user + logging.info("\n*** GET collection user -> ok") + _, headers, answer = self.request("GET", path_shared, check=200, login="user:userpw") + # check REPORT as user logging.info("\n*** REPORT collection user -> ok") _, responses = self.report(path_shared, """\ <?xml version="1.0" encoding="utf-8" ?> <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav"> <D:prop xmlns:D="DAV:"> + <D:getetag /> + </D:prop> +</C:calendar-query>""", login="user:userpw") + assert len(responses) == 1 + logging.info("response: %r", responses) + response = responses[path_shared_item] + assert isinstance(response, dict) + status, prop = response["D:getetag"] + assert status == 200 and prop.text + + # check GET as user without trailing / + logging.info("\n*** GET collection user (without trailing /) -> ok") + _, headers, answer = self.request("GET", path_shared.removesuffix("/"), check=200, login="user:userpw") + + # check REPORT as user without trailing / + logging.info("\n*** REPORT collection user (without trailing /) -> ok") + _, responses = self.report(path_shared.removesuffix("/"), """\ +<?xml version="1.0" encoding="utf-8" ?> +<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav"> + <D:prop xmlns:D="DAV:"> <D:getetag /> </D:prop> </C:calendar-query>""", login="user:userpw") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Radicale-3.7.7/setup.py.legacy new/Radicale-3.7.8/setup.py.legacy --- old/Radicale-3.7.7/setup.py.legacy 2026-07-19 16:31:27.000000000 +0200 +++ new/Radicale-3.7.8/setup.py.legacy 2026-08-06 08:01:32.000000000 +0200 @@ -21,7 +21,7 @@ # When the version is updated, a new section in the CHANGELOG.md file must be # added too. -VERSION = "3.7.7" +VERSION = "3.7.8" with open("README.md", encoding="utf-8") as f: long_description = f.read()
