1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-09-27 21:06:55 +00:00

Merge pull request #1831 from pbiering/fix-1824

Fix 1824
This commit is contained in:
Peter Bieringer 2025-07-20 18:04:08 +02:00 committed by GitHub
commit bb84b62c4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 138 additions and 1 deletions

View file

@ -10,6 +10,7 @@
* Fix: report with enabled expand honors now provided filter proper
* Improve: add options [logging] trace_on_debug and trace_filter for supporting trace logging
* Fix: catch case where getpwuid is not returning a username
* Fix: add support for query without comp-type
## 3.5.4
* Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute)

View file

@ -32,6 +32,7 @@ import vobject
from radicale import item, xmlutils
from radicale.log import logger
from radicale.utils import format_ut
DAY: timedelta = timedelta(days=1)
SECOND: timedelta = timedelta(seconds=1)
@ -98,6 +99,7 @@ def comp_match(item: "item.Item", filter_: ET.Element, level: int = 0) -> bool:
# HACK: the filters are tested separately against all components
name = filter_.get("name", "").upper()
logger.debug("TRACE/ITEM/FILTER/comp_match: name=%s level=%d", name, level)
if level == 0:
tag = item.name
@ -142,13 +144,24 @@ def comp_match(item: "item.Item", filter_: ET.Element, level: int = 0) -> bool:
trigger = subcomp.trigger.value
for child in filter_:
if child.tag == xmlutils.make_clark("C:prop-filter"):
logger.debug("TRACE/ITEM/FILTER/comp_match: prop-filter level=%d", level)
if not any(prop_match(comp, child, "C")
for comp in components):
return False
elif child.tag == xmlutils.make_clark("C:time-range"):
logger.debug("TRACE/ITEM/FILTER/comp_match: time-range level=%d tag=%s", level, tag)
if (level == 0) and (name == "VCALENDAR"):
for name_try in ("VTODO", "VEVENT", "VJOURNAL"):
try:
if time_range_match(item.vobject_item, filter_[0], name_try, trigger):
return True
except Exception:
continue
return False
if not time_range_match(item.vobject_item, filter_[0], tag, trigger):
return False
elif child.tag == xmlutils.make_clark("C:comp-filter"):
logger.debug("TRACE/ITEM/FILTER/comp_match: comp-filter level=%d", level)
if not comp_match(item, child, level=level + 1):
return False
else:
@ -233,6 +246,7 @@ def time_range_match(vobject_item: vobject.base.Component,
def infinity_fn(start: datetime) -> bool:
return False
logger.debug("TRACE/ITEM/FILTER/time_range_match: start=(%s) end=(%s) child_name=%s", start, end, child_name)
visit_time_ranges(vobject_item, child_name, range_fn, infinity_fn)
return matched
@ -289,6 +303,8 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
# recurrences too. This is not respected and client don't seem to bother
# either.
logger.debug("TRACE/ITEM/FILTER/visit_time_ranges: child_name=%s", child_name)
def getrruleset(child: vobject.base.Component, ignore: Sequence[date]
) -> Tuple[Iterable[date], bool]:
infinite = False
@ -516,6 +532,7 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
else:
# Match a property
logger.debug("TRACE/ITEM/FILTER/get_children: child_name=%s property match", child_name)
child = getattr(vobject_item, child_name.lower())
if isinstance(child.value, date):
child_is_datetime = isinstance(child.value, datetime)
@ -605,6 +622,7 @@ def simplify_prefilters(filters: Iterable[ET.Element], collection_tag: str
"""
flat_filters = list(chain.from_iterable(filters))
simple = len(flat_filters) <= 1
logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: collection_tag=%s", collection_tag)
for col_filter in flat_filters:
if collection_tag != "VCALENDAR":
simple = False
@ -615,7 +633,14 @@ def simplify_prefilters(filters: Iterable[ET.Element], collection_tag: str
continue
simple &= len(col_filter) <= 1
for comp_filter in col_filter:
logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: filter.tag=%s simple=%s", comp_filter.tag, simple)
if comp_filter.tag == xmlutils.make_clark("C:time-range") and simple is True:
# time-filter found on level 0
start, end = time_range_timestamps(comp_filter)
logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: found time-filter on level 0 start=%r(%d) end=%r(%d) simple=%s", format_ut(start), start, format_ut(end), end, simple)
return None, start, end, simple
if comp_filter.tag != xmlutils.make_clark("C:comp-filter"):
logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: no comp-filter on level 0")
simple = False
continue
tag = comp_filter.get("name", "").upper()
@ -632,6 +657,7 @@ def simplify_prefilters(filters: Iterable[ET.Element], collection_tag: str
simple = False
continue
start, end = time_range_timestamps(time_filter)
logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: found time-filter on level 1 tag=%s start=%d end=%d simple=%s", tag, start, end, simple)
return tag, start, end, simple
return tag, TIMESTAMP_MIN, TIMESTAMP_MAX, simple
return None, TIMESTAMP_MIN, TIMESTAMP_MAX, simple

View file

@ -2,7 +2,7 @@
# Copyright © 2014 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2022 Unrud <unrud@outlook.com>
# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -37,6 +37,7 @@ from radicale import item as radicale_item
from radicale import types, utils
from radicale.item import filter as radicale_filter
from radicale.log import logger
from radicale.utils import format_ut
INTERNAL_TYPES: Sequence[str] = ("multifilesystem", "multifilesystem_nolock",)
@ -153,12 +154,17 @@ class BaseCollection:
return
tag, start, end, simple = radicale_filter.simplify_prefilters(
filters, self.tag)
logger.debug("TRACE/STORAGE/get_filtered: prefilter tag=%s start=%s end=%s simple=%s", tag, format_ut(start), format_ut(end), simple)
for item in self.get_all():
logger.debug("TRACE/STORAGE/get_filtered: component_name=%s tag=%s", item.component_name, tag)
if tag is not None and tag != item.component_name:
continue
istart, iend = item.time_range
logger.debug("TRACE/STORAGE/get_filtered: istart=%s iend=%s", format_ut(istart), format_ut(iend))
if istart >= end or iend <= start:
logger.debug("TRACE/STORAGE/get_filtered: skip iuid=%s", item.uid)
continue
logger.debug("TRACE/STORAGE/get_filtered: add iuid=%s", item.uid)
yield item, simple and (start <= istart or iend <= end)
def has_uid(self, uid: str) -> bool:

View file

@ -1214,6 +1214,90 @@ permissions: RrWw""")
</C:comp-filter>"""], items=(9,))
assert "/calendar.ics/event9.ics" not in answer
def test_time_range_filter_without_comp_filter(self) -> None:
"""Report request with time-range filter without comp-filter on events."""
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</C:comp-filter>"""], "event", items=range(1, 6))
assert "/calendar.ics/event1.ics" in answer
assert "/calendar.ics/event2.ics" in answer
assert "/calendar.ics/event3.ics" in answer
assert "/calendar.ics/event4.ics" in answer
assert "/calendar.ics/event5.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20130902T000000Z" end="20131001T000000Z"/>
</C:comp-filter>"""], items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" in answer
assert "/calendar.ics/event3.ics" in answer
assert "/calendar.ics/event4.ics" in answer
assert "/calendar.ics/event5.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20130903T000000Z" end="20130908T000000Z"/>
</C:comp-filter>"""], items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" not in answer
assert "/calendar.ics/event3.ics" in answer
assert "/calendar.ics/event4.ics" in answer
assert "/calendar.ics/event5.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20130903T000000Z" end="20130904T000000Z"/>
</C:comp-filter>"""], items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" not in answer
assert "/calendar.ics/event3.ics" in answer
assert "/calendar.ics/event4.ics" not in answer
assert "/calendar.ics/event5.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20130805T000000Z" end="20130810T000000Z"/>
</C:comp-filter>"""], items=range(1, 6))
assert "/calendar.ics/event1.ics" not in answer
assert "/calendar.ics/event2.ics" not in answer
assert "/calendar.ics/event3.ics" not in answer
assert "/calendar.ics/event4.ics" not in answer
assert "/calendar.ics/event5.ics" not in answer
# HACK: VObject doesn't match RECURRENCE-ID to recurrences, the
# overwritten recurrence is still used for filtering.
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20170601T063000Z" end="20170601T070000Z"/>
</C:comp-filter>"""], items=(6, 7, 8, 9))
assert "/calendar.ics/event6.ics" in answer
assert "/calendar.ics/event7.ics" in answer
assert "/calendar.ics/event8.ics" in answer
assert "/calendar.ics/event9.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20170701T060000Z"/>
</C:comp-filter>"""], items=(6, 7, 8, 9))
assert "/calendar.ics/event6.ics" in answer
assert "/calendar.ics/event7.ics" in answer
assert "/calendar.ics/event8.ics" in answer
assert "/calendar.ics/event9.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20170702T070000Z" end="20170704T060000Z"/>
</C:comp-filter>"""], items=(6, 7, 8, 9))
assert "/calendar.ics/event6.ics" not in answer
assert "/calendar.ics/event7.ics" not in answer
assert "/calendar.ics/event8.ics" not in answer
assert "/calendar.ics/event9.ics" not in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20170602T075959Z" end="20170602T080000Z"/>
</C:comp-filter>"""], items=(9,))
assert "/calendar.ics/event9.ics" in answer
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:time-range start="20170602T080000Z" end="20170603T083000Z"/>
</C:comp-filter>"""], items=(9,))
assert "/calendar.ics/event9.ics" not in answer
def test_time_range_filter_events_rrule(self) -> None:
"""Report request with time-range filter on events with rrules."""
answer = self._test_filter(["""\

View file

@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
import datetime
import os
import ssl
import sys
@ -46,6 +47,10 @@ ADDRESS_TYPE = Union[Tuple[Union[str, bytes, bytearray], int],
Tuple[str, int, int, int]]
# Max YEAR in datetime in unixtime
DATETIME_MAX_UNIXTIME: int = (datetime.MAXYEAR - 1970) * 365 * 24 * 60 * 60
def load_plugin(internal_types: Sequence[str], module_name: str,
class_name: str, base_class: Type[_T_co],
configuration: "config.Configuration") -> _T_co:
@ -244,3 +249,18 @@ def user_groups_as_string():
username = os.getlogin()
s = "user=%s" % (username)
return s
def format_ut(unixtime: int) -> str:
if sys.platform == "win32":
# TODO check how to support this better
return str(unixtime)
if unixtime < DATETIME_MAX_UNIXTIME:
if sys.version_info < (3, 11):
dt = datetime.datetime.utcfromtimestamp(unixtime)
else:
dt = datetime.datetime.fromtimestamp(unixtime, datetime.UTC)
r = str(unixtime) + "(" + dt.strftime('%Y-%m-%dT%H:%M:%SZ') + ")"
else:
r = str(unixtime) + "(>MAX:" + str(DATETIME_MAX_UNIXTIME) + ")"
return r