mirror of
https://github.com/Kozea/Radicale.git
synced 2025-09-27 21:06:55 +00:00
commit
bb84b62c4a
5 changed files with 138 additions and 1 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(["""\
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue