mirror of
https://github.com/Kozea/Radicale.git
synced 2025-08-01 18:18:31 +00:00
Merge branch 'master' into email
This commit is contained in:
commit
0cc5d42947
14 changed files with 803 additions and 65 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -23,3 +23,5 @@ coverage.xml
|
|||
.vscode
|
||||
.sass-cache
|
||||
Gemfile.lock
|
||||
|
||||
pytestdebug.log
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
* Improve: display owner+permissions on directories on startup, extend error message in case of missing permissions
|
||||
* Feature: add hook for server-side e-mail notification
|
||||
* Fix: logging ignores not retrievable get_native_id if not supported by OS
|
||||
* Fix: report with enabled expand honors now provided filter proper
|
||||
* Improve: add options [logging] trace_on_debug and trace_filter for supporting trace logging
|
||||
|
||||
## 3.5.4
|
||||
* Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute)
|
||||
|
|
|
@ -1420,6 +1420,24 @@ Available levels: **debug**, **info**, **warning**, **error**, **critical**
|
|||
|
||||
Default: `warning` _(< 3.2.0)_ `info` _(>= 3.2.0)_
|
||||
|
||||
##### trace_on_debug
|
||||
|
||||
_(> 3.5.4)_
|
||||
|
||||
Do not filter debug messages starting with 'TRACE'
|
||||
|
||||
Default: `False`
|
||||
|
||||
##### trace_filter
|
||||
|
||||
_(> 3.5.4)_
|
||||
|
||||
Filter debug messages starting with 'TRACE/<TOKEN>'
|
||||
|
||||
Precondition: `trace_on_debug = True`
|
||||
|
||||
Default: (empty)
|
||||
|
||||
##### mask_passwords
|
||||
|
||||
Don't include passwords in logs.
|
||||
|
|
6
config
6
config
|
@ -272,6 +272,12 @@
|
|||
# Value: debug | info | warning | error | critical
|
||||
#level = info
|
||||
|
||||
# do not filter debug messages starting with 'TRACE'
|
||||
#trace_on_debug = False
|
||||
|
||||
# filter debug messages starting with 'TRACE/<TOKEN>'
|
||||
#trace_filter = ""
|
||||
|
||||
# Don't include passwords in logs
|
||||
#mask_passwords = True
|
||||
|
||||
|
|
|
@ -52,7 +52,10 @@ def _get_application_instance(config_path: str, wsgi_errors: types.ErrorStream
|
|||
configuration = config.load(config.parse_compound_paths(
|
||||
config.DEFAULT_CONFIG_PATH,
|
||||
config_path))
|
||||
log.set_level(cast(str, configuration.get("logging", "level")), configuration.get("logging", "backtrace_on_debug"))
|
||||
log.set_level(cast(str, configuration.get("logging", "level")),
|
||||
configuration.get("logging", "backtrace_on_debug"),
|
||||
configuration.get("logging", "trace_on_debug"),
|
||||
configuration.get("logging", "trace_filter"))
|
||||
# Log configuration after logger is configured
|
||||
default_config_active = True
|
||||
for source, miss in configuration.sources():
|
||||
|
|
|
@ -165,7 +165,10 @@ def run() -> None:
|
|||
sys.exit(1)
|
||||
|
||||
# Configure logging
|
||||
log.set_level(cast(str, configuration.get("logging", "level")), configuration.get("logging", "backtrace_on_debug"))
|
||||
log.set_level(cast(str, configuration.get("logging", "level")),
|
||||
configuration.get("logging", "backtrace_on_debug"),
|
||||
configuration.get("logging", "trace_on_debug"),
|
||||
configuration.get("logging", "trace_filter"))
|
||||
|
||||
# Log configuration after logger is configured
|
||||
default_config_active = True
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
# Copyright © 2017-2021 Unrud <unrud@outlook.com>
|
||||
# Copyright © 2024-2024 Pieter Hijma <pieterhijma@users.noreply.github.com>
|
||||
# Copyright © 2024-2024 Ray <ray@react0r.com>
|
||||
# Copyright © 2024-2024 Georgiy <metallerok@gmail.com>
|
||||
# Copyright © 2024-2025 Georgiy <metallerok@gmail.com>
|
||||
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
|
||||
# Copyright © 2025-2025 David Greaves <david@dgreaves.com>
|
||||
#
|
||||
# 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
|
||||
|
@ -144,7 +145,8 @@ def free_busy_report(base_prefix: str, path: str, xml_request: Optional[ET.Eleme
|
|||
|
||||
def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
|
||||
collection: storage.BaseCollection, encoding: str,
|
||||
unlock_storage_fn: Callable[[], None]
|
||||
unlock_storage_fn: Callable[[], None],
|
||||
max_occurrence: int = 0,
|
||||
) -> Tuple[int, ET.Element]:
|
||||
"""Read and answer REPORT requests that return XML.
|
||||
|
||||
|
@ -223,14 +225,34 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
|
|||
filters = (
|
||||
root.findall(xmlutils.make_clark("C:filter")) +
|
||||
root.findall(xmlutils.make_clark("CR:filter")))
|
||||
expand = root.find(".//" + xmlutils.make_clark("C:expand"))
|
||||
|
||||
# if we have expand prop we use "filter (except time range) -> expand -> filter (only time range)" approach
|
||||
time_range_element = None
|
||||
main_filters = []
|
||||
for filter_ in filters:
|
||||
# extract time-range filter for processing after main filters
|
||||
# for expand request
|
||||
filter_copy = copy.deepcopy(filter_)
|
||||
|
||||
if expand is not None:
|
||||
for comp_filter in filter_copy.findall(".//" + xmlutils.make_clark("C:comp-filter")):
|
||||
if comp_filter.get("name", "").upper() == "VCALENDAR":
|
||||
continue
|
||||
time_range_element = comp_filter.find(xmlutils.make_clark("C:time-range"))
|
||||
if time_range_element is not None:
|
||||
comp_filter.remove(time_range_element)
|
||||
|
||||
main_filters.append(filter_copy)
|
||||
|
||||
# Retrieve everything required for finishing the request.
|
||||
retrieved_items = list(retrieve_items(
|
||||
base_prefix, path, collection, hreferences, filters, multistatus))
|
||||
base_prefix, path, collection, hreferences, main_filters, multistatus))
|
||||
collection_tag = collection.tag
|
||||
# !!! Don't access storage after this !!!
|
||||
unlock_storage_fn()
|
||||
|
||||
n_vevents = 0
|
||||
while retrieved_items:
|
||||
# ``item.vobject_item`` might be accessed during filtering.
|
||||
# Don't keep reference to ``item``, because VObject requires a lot of
|
||||
|
@ -239,7 +261,7 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
|
|||
if filters and not filters_matched:
|
||||
try:
|
||||
if not all(test_filter(collection_tag, item, filter_)
|
||||
for filter_ in filters):
|
||||
for filter_ in main_filters):
|
||||
continue
|
||||
except ValueError as e:
|
||||
raise ValueError("Failed to filter item %r from %r: %s" %
|
||||
|
@ -264,36 +286,59 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
|
|||
xmlutils.make_clark("CR:address-data")):
|
||||
element.text = item.serialize()
|
||||
|
||||
expand = prop.find(xmlutils.make_clark("C:expand"))
|
||||
if expand is not None and item.component_name == 'VEVENT':
|
||||
start = expand.get('start')
|
||||
end = expand.get('end')
|
||||
if (expand is not None) and item.component_name == 'VEVENT':
|
||||
starts = expand.get('start')
|
||||
ends = expand.get('end')
|
||||
|
||||
if (start is None) or (end is None):
|
||||
if (starts is None) or (ends is None):
|
||||
return client.FORBIDDEN, \
|
||||
xmlutils.webdav_error("C:expand")
|
||||
|
||||
start = datetime.datetime.strptime(
|
||||
start, '%Y%m%dT%H%M%SZ'
|
||||
starts, '%Y%m%dT%H%M%SZ'
|
||||
).replace(tzinfo=datetime.timezone.utc)
|
||||
end = datetime.datetime.strptime(
|
||||
end, '%Y%m%dT%H%M%SZ'
|
||||
ends, '%Y%m%dT%H%M%SZ'
|
||||
).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
expanded_element = _expand(
|
||||
element, copy.copy(item), start, end)
|
||||
time_range_start = None
|
||||
time_range_end = None
|
||||
|
||||
if time_range_element is not None:
|
||||
time_range_start, time_range_end = radicale_filter.parse_time_range(time_range_element)
|
||||
|
||||
(expanded_element, n_vev) = _expand(
|
||||
element=element, item=copy.copy(item),
|
||||
start=start, end=end,
|
||||
time_range_start=time_range_start, time_range_end=time_range_end,
|
||||
max_occurrence=max_occurrence,
|
||||
)
|
||||
|
||||
if n_vev == 0:
|
||||
logger.debug("No VEVENTs found after expansion for %r, skipping", item.href)
|
||||
continue
|
||||
|
||||
n_vevents += n_vev
|
||||
found_props.append(expanded_element)
|
||||
else:
|
||||
found_props.append(element)
|
||||
if hasattr(item.vobject_item, "vevent_list"):
|
||||
n_vevents += len(item.vobject_item.vevent_list)
|
||||
# Avoid DoS with too many events
|
||||
if max_occurrence and n_vevents > max_occurrence:
|
||||
raise ValueError("REPORT occurrences limit of {} hit"
|
||||
.format(max_occurrence))
|
||||
else:
|
||||
not_found_props.append(element)
|
||||
|
||||
assert item.href
|
||||
uri = pathutils.unstrip_path(
|
||||
posixpath.join(collection.path, item.href))
|
||||
multistatus.append(xml_item_response(
|
||||
base_prefix, uri, found_props=found_props,
|
||||
not_found_props=not_found_props, found_item=True))
|
||||
|
||||
if found_props or not_found_props:
|
||||
multistatus.append(xml_item_response(
|
||||
base_prefix, uri, found_props=found_props,
|
||||
not_found_props=not_found_props, found_item=True))
|
||||
|
||||
return client.MULTI_STATUS, multistatus
|
||||
|
||||
|
@ -303,8 +348,14 @@ def _expand(
|
|||
item: radicale_item.Item,
|
||||
start: datetime.datetime,
|
||||
end: datetime.datetime,
|
||||
) -> ET.Element:
|
||||
time_range_start: Optional[datetime.datetime] = None,
|
||||
time_range_end: Optional[datetime.datetime] = None,
|
||||
max_occurrence: int = 0,
|
||||
) -> Tuple[ET.Element, int]:
|
||||
vevent_component: vobject.base.Component = copy.copy(item.vobject_item)
|
||||
logger.info("Expanding event %s", item.href)
|
||||
logger.debug(f"Expand range: {start} to {end}")
|
||||
logger.debug(f"Time range: {time_range_start} to {time_range_end}")
|
||||
|
||||
# Split the vevents included in the component into one that contains the
|
||||
# recurrence information and others that contain a recurrence id to
|
||||
|
@ -323,6 +374,9 @@ def _expand(
|
|||
# rruleset.between computes with datetimes without timezone information
|
||||
start = start.replace(tzinfo=None)
|
||||
end = end.replace(tzinfo=None)
|
||||
if time_range_start is not None and time_range_end is not None:
|
||||
time_range_start = time_range_start.replace(tzinfo=None)
|
||||
time_range_end = time_range_end.replace(tzinfo=None)
|
||||
|
||||
for vevent in vevents_overridden:
|
||||
_strip_single_event(vevent, dt_format)
|
||||
|
@ -330,32 +384,92 @@ def _expand(
|
|||
duration = None
|
||||
if hasattr(vevent_recurrence, "dtend"):
|
||||
duration = vevent_recurrence.dtend.value - vevent_recurrence.dtstart.value
|
||||
elif hasattr(vevent_recurrence, "duration"):
|
||||
try:
|
||||
duration = vevent_recurrence.duration.value
|
||||
if duration.total_seconds() <= 0:
|
||||
logger.warning("Invalid DURATION: %s", duration)
|
||||
duration = None
|
||||
except (AttributeError, TypeError) as e:
|
||||
logger.warning("Failed to parse DURATION: %s", e)
|
||||
duration = None
|
||||
|
||||
# Generate EXDATE to remove from expansion range
|
||||
exdates_set: set[datetime.datetime] = set()
|
||||
if hasattr(vevent_recurrence, 'exdate'):
|
||||
exdates = vevent_recurrence.exdate.value
|
||||
if not isinstance(exdates, list):
|
||||
exdates = [exdates]
|
||||
|
||||
exdates_set = {
|
||||
exdate.astimezone(datetime.timezone.utc) if isinstance(exdate, datetime.datetime)
|
||||
else datetime.datetime.fromordinal(exdate.toordinal()).replace(tzinfo=None)
|
||||
for exdate in exdates
|
||||
}
|
||||
|
||||
logger.debug("EXDATE values: %s", exdates_set)
|
||||
|
||||
rruleset = None
|
||||
if hasattr(vevent_recurrence, 'rrule'):
|
||||
rruleset = vevent_recurrence.getrruleset()
|
||||
|
||||
filtered_vevents = []
|
||||
if rruleset:
|
||||
# This function uses datetimes internally without timezone info for dates
|
||||
recurrences = rruleset.between(start, end, inc=True)
|
||||
|
||||
# A vobject rruleset is for the event dtstart.
|
||||
# Expanded over a given time range this will not include
|
||||
# events which started before the time range but are still
|
||||
# ongoing at the start of the range
|
||||
|
||||
# To accomodate this, reduce the start time by the duration of
|
||||
# the event. If this introduces an extra reccurence point then
|
||||
# that event should be included as it is still ongoing. If no
|
||||
# extra point is generated then it was a no-op.
|
||||
rstart = start - duration if duration and duration.total_seconds() > 0 else start
|
||||
recurrences = rruleset.between(rstart, end, inc=True, count=max_occurrence)
|
||||
if max_occurrence and len(recurrences) >= max_occurrence:
|
||||
# this shouldn't be > and if it's == then assume a limit
|
||||
# was hit and ignore that maybe some would be filtered out
|
||||
# by EXDATE etc. This is anti-DoS, not precise limits
|
||||
raise ValueError("REPORT occurrences limit of {} hit"
|
||||
.format(max_occurrence))
|
||||
|
||||
_strip_component(vevent_component)
|
||||
_strip_single_event(vevent_recurrence, dt_format)
|
||||
|
||||
is_component_filled: bool = False
|
||||
i_overridden = 0
|
||||
|
||||
for recurrence_dt in recurrences:
|
||||
recurrence_utc = recurrence_dt.astimezone(datetime.timezone.utc)
|
||||
recurrence_utc = recurrence_dt if all_day_event else recurrence_dt.astimezone(datetime.timezone.utc)
|
||||
logger.debug("Processing recurrence: %s (all_day_event: %s)", recurrence_utc, all_day_event)
|
||||
|
||||
# Apply time-range filter
|
||||
if time_range_start is not None and time_range_end is not None:
|
||||
dtstart = recurrence_utc
|
||||
dtend = dtstart + duration if duration else dtstart
|
||||
# Start includes the time, end does not
|
||||
if not (dtstart <= time_range_end and dtend > time_range_start):
|
||||
logger.debug("Recurrence %s filtered out by time-range", recurrence_utc)
|
||||
continue
|
||||
|
||||
# Check exdate
|
||||
if recurrence_utc in exdates_set:
|
||||
logger.debug("Recurrence %s excluded by EXDATE", recurrence_utc)
|
||||
continue
|
||||
|
||||
# Check for overridden instances
|
||||
i_overridden, vevent = _find_overridden(i_overridden, vevents_overridden, recurrence_utc, dt_format)
|
||||
|
||||
if not vevent:
|
||||
# We did not find an overridden instance, so create a new one
|
||||
# Create new instance from recurrence
|
||||
vevent = copy.deepcopy(vevent_recurrence)
|
||||
|
||||
# For all day events, the system timezone may influence the
|
||||
# results, so use recurrence_dt
|
||||
recurrence_id = recurrence_dt if all_day_event else recurrence_utc
|
||||
logger.debug("Creating new VEVENT with RECURRENCE-ID: %s", recurrence_id)
|
||||
|
||||
vevent.recurrence_id = ContentLine(
|
||||
name='RECURRENCE-ID',
|
||||
value=recurrence_id, params={}
|
||||
|
@ -365,21 +479,67 @@ def _expand(
|
|||
name='DTSTART',
|
||||
value=recurrence_id.strftime(dt_format), params={}
|
||||
)
|
||||
if duration:
|
||||
# if there is a DTEND, override it. Duration does not need changing
|
||||
if hasattr(vevent, "dtend"):
|
||||
vevent.dtend = ContentLine(
|
||||
name='DTEND',
|
||||
value=(recurrence_id + duration).strftime(dt_format), params={}
|
||||
)
|
||||
|
||||
if not is_component_filled:
|
||||
vevent_component.vevent = vevent
|
||||
is_component_filled = True
|
||||
else:
|
||||
vevent_component.add(vevent)
|
||||
filtered_vevents.append(vevent)
|
||||
|
||||
# Filter overridden and recurrence base events
|
||||
if time_range_start is not None and time_range_end is not None:
|
||||
for vevent in vevents_overridden:
|
||||
dtstart = vevent.dtstart.value
|
||||
|
||||
# Handle string values for DTSTART/DTEND
|
||||
if isinstance(dtstart, str):
|
||||
try:
|
||||
dtstart = datetime.datetime.strptime(dtstart, dt_format)
|
||||
if all_day_event:
|
||||
dtstart = dtstart.date()
|
||||
except ValueError as e:
|
||||
logger.warning("Invalid DTSTART format: %s, error: %s", dtstart, e)
|
||||
continue
|
||||
|
||||
dtend = dtstart + duration if duration else dtstart
|
||||
|
||||
logger.debug(
|
||||
"Filtering VEVENT with DTSTART: %s (type: %s), DTEND: %s (type: %s)",
|
||||
dtstart, type(dtstart), dtend, type(dtend))
|
||||
|
||||
# Convert to datetime for comparison
|
||||
if all_day_event and isinstance(dtstart, datetime.date) and not isinstance(dtstart, datetime.datetime):
|
||||
dtstart = datetime.datetime.fromordinal(dtstart.toordinal()).replace(tzinfo=None)
|
||||
dtend = datetime.datetime.fromordinal(dtend.toordinal()).replace(tzinfo=None)
|
||||
elif not all_day_event and isinstance(dtstart, datetime.datetime) \
|
||||
and isinstance(dtend, datetime.datetime):
|
||||
dtstart = dtstart.replace(tzinfo=datetime.timezone.utc)
|
||||
dtend = dtend.replace(tzinfo=datetime.timezone.utc)
|
||||
else:
|
||||
logger.warning("Unexpected DTSTART/DTEND type: dtstart=%s, dtend=%s", type(dtstart), type(dtend))
|
||||
continue
|
||||
|
||||
if dtstart < time_range_end and dtend > time_range_start:
|
||||
if vevent not in filtered_vevents: # Avoid duplicates
|
||||
logger.debug("VEVENT passed time-range filter: %s", dtstart)
|
||||
filtered_vevents.append(vevent)
|
||||
else:
|
||||
logger.debug("VEVENT filtered out: %s", dtstart)
|
||||
|
||||
# Rebuild component
|
||||
|
||||
if not filtered_vevents:
|
||||
element.text = ""
|
||||
return element, 0
|
||||
else:
|
||||
vevent_component.vevent_list = filtered_vevents
|
||||
logger.debug("lbt: vevent_component %s", vevent_component)
|
||||
|
||||
element.text = vevent_component.serialize()
|
||||
|
||||
return element
|
||||
return element, len(filtered_vevents)
|
||||
|
||||
|
||||
def _convert_timezone(vevent: vobject.icalendar.RecurringComponent,
|
||||
|
@ -616,9 +776,9 @@ class ApplicationPartReport(ApplicationBase):
|
|||
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,
|
||||
|
@ -633,7 +793,7 @@ class ApplicationPartReport(ApplicationBase):
|
|||
try:
|
||||
status, xml_answer = xml_report(
|
||||
base_prefix, path, xml_content, collection, self._encoding,
|
||||
lock_stack.close)
|
||||
lock_stack.close, max_occurrence)
|
||||
except ValueError as e:
|
||||
logger.warning(
|
||||
"Bad REPORT request on %r: %s", path, e, exc_info=True)
|
||||
|
|
|
@ -520,6 +520,14 @@ This is an automated message. Please do not reply.""",
|
|||
"value": "info",
|
||||
"help": "threshold for the logger",
|
||||
"type": logging_level}),
|
||||
("trace_on_debug", {
|
||||
"value": "False",
|
||||
"help": "do not filter debug messages starting with 'TRACE'",
|
||||
"type": bool}),
|
||||
("trace_filter", {
|
||||
"value": "",
|
||||
"help": "filter debug messages starting with 'TRACE/<TOKEN>'",
|
||||
"type": str}),
|
||||
("bad_put_request_content", {
|
||||
"value": "False",
|
||||
"help": "log bad PUT request content",
|
||||
|
|
|
@ -57,8 +57,35 @@ class RemoveTracebackFilter(logging.Filter):
|
|||
return True
|
||||
|
||||
|
||||
class RemoveTRACEFilter(logging.Filter):
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
if record.msg.startswith("TRACE"):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class PassTRACETOKENFilter(logging.Filter):
|
||||
def __init__(self, trace_filter: str):
|
||||
super().__init__()
|
||||
self.trace_filter = trace_filter
|
||||
self.prefix = "TRACE/" + self.trace_filter
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
if record.msg.startswith("TRACE"):
|
||||
if record.msg.startswith(self.prefix):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
REMOVE_TRACEBACK_FILTER: logging.Filter = RemoveTracebackFilter()
|
||||
|
||||
REMOVE_TRACE_FILTER: logging.Filter = RemoveTRACEFilter()
|
||||
|
||||
|
||||
class IdentLogRecordFactory:
|
||||
"""LogRecordFactory that adds ``ident`` attribute."""
|
||||
|
@ -231,7 +258,7 @@ logger_display_backtrace_disabled: bool = False
|
|||
logger_display_backtrace_enabled: bool = False
|
||||
|
||||
|
||||
def set_level(level: Union[int, str], backtrace_on_debug: bool) -> None:
|
||||
def set_level(level: Union[int, str], backtrace_on_debug: bool, trace_on_debug: bool = False, trace_filter: str = "") -> None:
|
||||
"""Set logging level for global logger."""
|
||||
global logger_display_backtrace_disabled
|
||||
global logger_display_backtrace_enabled
|
||||
|
@ -255,3 +282,14 @@ def set_level(level: Union[int, str], backtrace_on_debug: bool) -> None:
|
|||
logger.debug("Logging of backtrace is enabled by option in this loglevel")
|
||||
logger_display_backtrace_enabled = True
|
||||
logger.removeFilter(REMOVE_TRACEBACK_FILTER)
|
||||
if trace_on_debug:
|
||||
if trace_filter != "":
|
||||
logger.debug("Logging messages starting with 'TRACE/%s' enabled", trace_filter)
|
||||
logger.addFilter(PassTRACETOKENFilter(trace_filter))
|
||||
logger.removeFilter(REMOVE_TRACE_FILTER)
|
||||
else:
|
||||
logger.debug("Logging messages starting with 'TRACE' enabled")
|
||||
logger.removeFilter(REMOVE_TRACE_FILTER)
|
||||
else:
|
||||
logger.debug("Logging messages starting with 'TRACE' disabled")
|
||||
logger.addFilter(REMOVE_TRACE_FILTER)
|
||||
|
|
|
@ -340,6 +340,8 @@ def serve(configuration: config.Configuration,
|
|||
select_timeout = 1.0
|
||||
max_connections: int = configuration.get("server", "max_connections")
|
||||
logger.info("Radicale server ready")
|
||||
logger.debug("TRACE: Radicale server ready ('logging/trace_on_debug' is active)")
|
||||
logger.debug("TRACE/SERVER: Radicale server ready ('logging/trace_on_debug' is active - either with 'SERVER' or empty filter)")
|
||||
while True:
|
||||
rlist: List[socket.socket] = []
|
||||
# Wait for finished clients
|
||||
|
|
28
radicale/tests/static/event_daily_rrule_forever.ics
Normal file
28
radicale/tests/static/event_daily_rrule_forever.ics
Normal file
|
@ -0,0 +1,28 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
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:20060102T120000
|
||||
DURATION:PT1H
|
||||
RRULE:FREQ=DAILY
|
||||
SUMMARY:Recurring event
|
||||
UID:event_daily_rrule_forever
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
129
radicale/tests/static/event_issue1812.ics
Normal file
129
radicale/tests/static/event_issue1812.ics
Normal file
|
@ -0,0 +1,129 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:DAVx5/4.4.6-beta.1-ose ical4j/3.2.19
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/London
|
||||
BEGIN:STANDARD
|
||||
DTSTART:19961027T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
TZNAME:GMT
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0000
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:19810329T010000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
TZNAME:BST
|
||||
TZOFFSETFROM:+0000
|
||||
TZOFFSETTO:+0100
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
DTSTART;TZID=Europe/London:20230101T180000
|
||||
DTEND;TZID=Europe/London:20230101T233000
|
||||
CREATED:20230130T181142Z
|
||||
DTSTAMP:20250515T182647Z
|
||||
EXDATE;TZID=Europe/London:20231222T180000,20240112T180000,20240126T180000,2
|
||||
0240329T180000,20241018T180000,20241129T180000,20241206T180000,20241213T18
|
||||
0000
|
||||
EXDATE;TZID=Europe/London:20250521T180000
|
||||
EXDATE;TZID=Europe/London:20250515T180000
|
||||
RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:3EF0E463-40EB-47FF-B825-
|
||||
D474CE894708
|
||||
RRULE:FREQ=DAILY
|
||||
SEQUENCE:11
|
||||
SUMMARY:TV Room
|
||||
X-MOZ-GENERATION:23
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20240113T180000
|
||||
DTSTART;TZID=Europe/London:20240113T183000
|
||||
DTEND;TZID=Europe/London:20240113T230000
|
||||
DTSTAMP:20250515T182647Z
|
||||
SEQUENCE:5
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20231227T180000
|
||||
DTSTART;TZID=Europe/London:20231227T203000
|
||||
DTEND;TZID=Europe/London:20231227T233000
|
||||
DTSTAMP:20250515T182647Z
|
||||
SEQUENCE:3
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20231126T180000
|
||||
DTSTART;TZID=Europe/London:20231126T180000
|
||||
DTEND;TZID=Europe/London:20231126T223000
|
||||
DTSTAMP:20250515T182647Z
|
||||
SEQUENCE:3
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20231225T180000
|
||||
DTSTART;TZID=Europe/London:20231225T211500
|
||||
DTEND;TZID=Europe/London:20231225T233000
|
||||
DTSTAMP:20250515T182647Z
|
||||
SEQUENCE:3
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20231129T180000
|
||||
DTSTART;TZID=Europe/London:20231129T173000
|
||||
DTEND;TZID=Europe/London:20231129T233000
|
||||
DTSTAMP:20250515T182647Z
|
||||
SEQUENCE:2
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20240220T180000
|
||||
DTSTART;TZID=Europe/London:20240220T173000
|
||||
DTEND;TZID=Europe/London:20240220T233000
|
||||
DTSTAMP:20250515T182647Z
|
||||
SEQUENCE:5
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20240310T180000
|
||||
DTSTART;TZID=Europe/London:20240310T174500
|
||||
DTEND;TZID=Europe/London:20240310T233000
|
||||
DTSTAMP:20250515T182647Z
|
||||
SEQUENCE:5
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20240324T180000
|
||||
DTSTART;TZID=Europe/London:20240324T183000
|
||||
DTEND;TZID=Europe/London:20240324T233000
|
||||
DTSTAMP:20250515T182648Z
|
||||
SEQUENCE:6
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20241027T180000
|
||||
DTSTART;TZID=Europe/London:20241027T173000
|
||||
DTEND;TZID=Europe/London:20241027T233000
|
||||
DTSTAMP:20250515T182648Z
|
||||
SEQUENCE:7
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_issue1812
|
||||
RECURRENCE-ID;TZID=Europe/London:20241226T180000
|
||||
DTSTART;TZID=Europe/London:20241226T193000
|
||||
DTEND;TZID=Europe/London:20241227T003000
|
||||
DTSTAMP:20250515T182648Z
|
||||
SEQUENCE:10
|
||||
SUMMARY:TV Room
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
100
radicale/tests/static/event_multiple_too_many.ics
Normal file
100
radicale/tests/static/event_multiple_too_many.ics
Normal file
|
@ -0,0 +1,100 @@
|
|||
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
|
||||
UID:event_multiple_too_many
|
||||
SUMMARY:Event
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many1
|
||||
SUMMARY:Event1
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many2
|
||||
SUMMARY:Event2
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many3
|
||||
SUMMARY:Event3
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many4
|
||||
SUMMARY:Event4
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many5
|
||||
SUMMARY:Event5
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many6
|
||||
SUMMARY:Event6
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many7
|
||||
SUMMARY:Event7
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many8
|
||||
SUMMARY:Event8
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many9
|
||||
SUMMARY:Event9
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many10
|
||||
SUMMARY:Event10
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:event_multiple_too_many11
|
||||
SUMMARY:Event11
|
||||
DTSTART;TZID=Europe/Paris:20130901T190000
|
||||
DTEND;TZID=Europe/Paris:20130901T200000
|
||||
END:VEVENT
|
||||
BEGIN:VTODO
|
||||
UID:todo
|
||||
DTSTART;TZID=Europe/Paris:20130901T220000
|
||||
DURATION:PT1H
|
||||
SUMMARY:Todo
|
||||
END:VTODO
|
||||
END:VCALENDAR
|
|
@ -1,6 +1,8 @@
|
|||
# This file is part of Radicale - CalDAV and CardDAV server
|
||||
# Copyright © 2012-2017 Guillaume Ayoub
|
||||
# Copyright © 2017-2019 Unrud <unrud@outlook.com>
|
||||
# Copyright © 2024 Pieter Hijma <pieterhijma@users.noreply.github.com>
|
||||
# Copyright © 2025 David Greaves <david@dgreaves.com>
|
||||
#
|
||||
# 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
|
||||
|
@ -21,8 +23,10 @@ Radicale tests with expand requests.
|
|||
"""
|
||||
|
||||
import os
|
||||
from typing import ClassVar, List
|
||||
from typing import ClassVar, List, Optional
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from radicale.log import logger
|
||||
from radicale.tests import BaseTest
|
||||
from radicale.tests.helpers import get_file_content
|
||||
|
||||
|
@ -68,17 +72,13 @@ permissions: RrWw""")
|
|||
self.configure({"rights": {"file": rights_file_path,
|
||||
"type": "from_file"}})
|
||||
|
||||
def _test_expand(self,
|
||||
expected_uid: str,
|
||||
start: str,
|
||||
end: str,
|
||||
expected_recurrence_ids: List[str],
|
||||
expected_start_times: List[str],
|
||||
expected_end_times: List[str],
|
||||
only_dates: bool,
|
||||
nr_uids: int) -> None:
|
||||
def _req_without_expand(self,
|
||||
expected_uid: str,
|
||||
start: str,
|
||||
end: str,
|
||||
) -> str:
|
||||
self.put("/calendar.ics/", get_file_content(f"{expected_uid}.ics"))
|
||||
req_body_without_expand = \
|
||||
return \
|
||||
f"""<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
|
@ -94,9 +94,43 @@ permissions: RrWw""")
|
|||
</C:filter>
|
||||
</C:calendar-query>
|
||||
"""
|
||||
_, responses = self.report("/calendar.ics/", req_body_without_expand)
|
||||
assert len(responses) == 1
|
||||
|
||||
def _req_with_expand(self,
|
||||
expected_uid: str,
|
||||
start: str,
|
||||
end: str,
|
||||
) -> str:
|
||||
self.put("/calendar.ics/", get_file_content(f"{expected_uid}.ics"))
|
||||
return \
|
||||
f"""<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<C:calendar-data>
|
||||
<C:expand start="{start}" end="{end}"/>
|
||||
</C:calendar-data>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:time-range start="{start}" end="{end}"/>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>
|
||||
"""
|
||||
|
||||
def _test_expand(self,
|
||||
expected_uid: str,
|
||||
start: str,
|
||||
end: str,
|
||||
expected_recurrence_ids: List[str],
|
||||
expected_start_times: List[str],
|
||||
expected_end_times: List[str],
|
||||
only_dates: bool,
|
||||
nr_uids: int) -> None:
|
||||
_, responses = self.report("/calendar.ics/",
|
||||
self._req_without_expand(expected_uid, start, end))
|
||||
assert len(responses) == 1
|
||||
response_without_expand = responses[f'/calendar.ics/{expected_uid}.ics']
|
||||
assert not isinstance(response_without_expand, int)
|
||||
status, element = response_without_expand["C:calendar-data"]
|
||||
|
@ -118,25 +152,8 @@ permissions: RrWw""")
|
|||
|
||||
assert len(uids) == nr_uids
|
||||
|
||||
req_body_with_expand = \
|
||||
f"""<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<C:calendar-data>
|
||||
<C:expand start="{start}" end="{end}"/>
|
||||
</C:calendar-data>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:time-range start="{start}" end="{end}"/>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>
|
||||
"""
|
||||
|
||||
_, responses = self.report("/calendar.ics/", req_body_with_expand)
|
||||
_, responses = self.report("/calendar.ics/",
|
||||
self._req_with_expand(expected_uid, start, end))
|
||||
|
||||
assert len(responses) == 1
|
||||
|
||||
|
@ -144,6 +161,8 @@ permissions: RrWw""")
|
|||
assert not isinstance(response_with_expand, int)
|
||||
status, element = response_with_expand["C:calendar-data"]
|
||||
|
||||
logger.debug("lbt: element is %s",
|
||||
ElementTree.tostring(element, encoding='unicode'))
|
||||
assert status == 200 and element.text
|
||||
assert "RRULE" not in element.text
|
||||
assert "BEGIN:VTIMEZONE" not in element.text
|
||||
|
@ -168,6 +187,29 @@ permissions: RrWw""")
|
|||
assert len(uids) == len(expected_recurrence_ids)
|
||||
assert len(set(recurrence_ids)) == len(expected_recurrence_ids)
|
||||
|
||||
def _test_expand_max(self,
|
||||
expected_uid: str,
|
||||
start: str,
|
||||
end: str,
|
||||
check: Optional[int] = None) -> None:
|
||||
_, responses = self.report("/calendar.ics/",
|
||||
self._req_without_expand(expected_uid, start, end))
|
||||
assert len(responses) == 1
|
||||
response_without_expand = responses[f'/calendar.ics/{expected_uid}.ics']
|
||||
assert not isinstance(response_without_expand, int)
|
||||
status, element = response_without_expand["C:calendar-data"]
|
||||
|
||||
assert status == 200 and element.text
|
||||
|
||||
assert "RRULE" in element.text
|
||||
|
||||
status, _, _ = self.request(
|
||||
"REPORT", "/calendar.ics/",
|
||||
self._req_with_expand(expected_uid, start, end),
|
||||
check=check)
|
||||
|
||||
assert status == 400
|
||||
|
||||
def test_report_with_expand_property(self) -> None:
|
||||
"""Test report with expand property"""
|
||||
self._test_expand(
|
||||
|
@ -181,6 +223,58 @@ permissions: RrWw""")
|
|||
1
|
||||
)
|
||||
|
||||
def test_report_with_expand_property_start_inside(self) -> None:
|
||||
"""Test report with expand property start inside"""
|
||||
self._test_expand(
|
||||
"event_daily_rrule",
|
||||
"20060103T171500Z",
|
||||
"20060105T000000Z",
|
||||
["RECURRENCE-ID:20060103T170000Z", "RECURRENCE-ID:20060104T170000Z"],
|
||||
["DTSTART:20060103T170000Z", "DTSTART:20060104T170000Z"],
|
||||
[],
|
||||
CONTAINS_TIMES,
|
||||
1
|
||||
)
|
||||
|
||||
def test_report_with_expand_property_just_inside(self) -> None:
|
||||
"""Test report with expand property start and end inside event"""
|
||||
self._test_expand(
|
||||
"event_daily_rrule",
|
||||
"20060103T171500Z",
|
||||
"20060103T171501Z",
|
||||
["RECURRENCE-ID:20060103T170000Z"],
|
||||
["DTSTART:20060103T170000Z"],
|
||||
[],
|
||||
CONTAINS_TIMES,
|
||||
1
|
||||
)
|
||||
|
||||
def test_report_with_expand_property_issue1812(self) -> None:
|
||||
"""Test report with expand property for issue 1812"""
|
||||
self._test_expand(
|
||||
"event_issue1812",
|
||||
"20250127T183000Z",
|
||||
"20250127T183001Z",
|
||||
["RECURRENCE-ID:20250127T180000Z"],
|
||||
["DTSTART:20250127T180000Z"],
|
||||
["DTEND:20250127T233000Z"],
|
||||
CONTAINS_TIMES,
|
||||
11
|
||||
)
|
||||
|
||||
def test_report_with_expand_property_issue1812_DS(self) -> None:
|
||||
"""Test report with expand property for issue 1812 - DS active"""
|
||||
self._test_expand(
|
||||
"event_issue1812",
|
||||
"20250627T183000Z",
|
||||
"20250627T183001Z",
|
||||
["RECURRENCE-ID:20250627T170000Z"],
|
||||
["DTSTART:20250627T170000Z"],
|
||||
["DTEND:20250627T223000Z"],
|
||||
CONTAINS_TIMES,
|
||||
11
|
||||
)
|
||||
|
||||
def test_report_with_expand_property_all_day_event(self) -> None:
|
||||
"""Test report with expand property for all day events"""
|
||||
self._test_expand(
|
||||
|
@ -228,3 +322,148 @@ permissions: RrWw""")
|
|||
CONTAINS_TIMES,
|
||||
1
|
||||
)
|
||||
|
||||
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._test_expand_max(
|
||||
"event_daily_rrule_forever",
|
||||
"20060103T000000Z",
|
||||
"20060501T000000Z",
|
||||
check=400
|
||||
)
|
||||
|
||||
def test_report_with_max_occur(self) -> None:
|
||||
"""Test report with too many vevents"""
|
||||
self.configure({"reporting": {"max_freebusy_occurrence": 10}})
|
||||
|
||||
uid = "event_multiple_too_many"
|
||||
start = "20130901T000000Z"
|
||||
end = "20130902T000000Z"
|
||||
check = 400
|
||||
|
||||
status, responses = self.report("/calendar.ics/",
|
||||
self._req_without_expand(uid, start, end),
|
||||
check=check)
|
||||
assert len(responses) == 0
|
||||
assert status == check
|
||||
|
||||
def test_report_vcalendar_all_components(self) -> None:
|
||||
"""Test calendar-query with comp-filter VCALENDAR, returns all components."""
|
||||
self.mkcalendar("/test/")
|
||||
self.put("/test/calendar.ics", get_file_content("event_daily_rrule.ics"))
|
||||
self.put("/test/todo.ics", get_file_content("todo1.ics"))
|
||||
|
||||
request = """
|
||||
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<C:calendar-data/>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR"/>
|
||||
</C:filter>
|
||||
</C:calendar-query>
|
||||
"""
|
||||
status, responses = self.report("/test", request)
|
||||
assert status == 207
|
||||
assert len(responses) == 2
|
||||
assert "/test/calendar.ics" in responses
|
||||
assert "/test/todo.ics" in responses
|
||||
|
||||
def test_report_vevent_only(self) -> None:
|
||||
"""Test calendar-query with comp-filter VEVENT, returns only VEVENT."""
|
||||
self.mkcalendar("/test/")
|
||||
self.put("/test/calendar.ics", get_file_content("event_daily_rrule.ics"))
|
||||
self.put("/test/todo.ics", get_file_content("todo1.ics"))
|
||||
|
||||
start = "20060101T000000Z"
|
||||
end = "20060104T000000Z"
|
||||
|
||||
request = f"""
|
||||
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<C:calendar-data>
|
||||
<C:expand start="{start}" end="{end}"/>
|
||||
</C:calendar-data>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:time-range start="{start}" end="{end}"/>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>
|
||||
"""
|
||||
status, responses = self.report("/test", request)
|
||||
assert status == 207
|
||||
assert len(responses) == 1
|
||||
assert "/test/calendar.ics" in responses
|
||||
vevent_response = responses["/test/calendar.ics"]
|
||||
assert type(vevent_response) is dict
|
||||
status, element = vevent_response["C:calendar-data"]
|
||||
assert status == 200 and element.text
|
||||
assert "BEGIN:VEVENT" in element.text
|
||||
assert "UID:" in element.text
|
||||
assert "BEGIN:VTODO" not in element.text
|
||||
|
||||
def test_report_time_range_no_vevent(self) -> None:
|
||||
"""Test calendar-query with time-range filter, no matching VEVENT."""
|
||||
self.mkcalendar("/test/")
|
||||
self.put("/test/calendar.ics/", get_file_content("event_daily_rrule.ics"))
|
||||
|
||||
request = """
|
||||
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<C:calendar-data>
|
||||
<C:expand start="20000101T000000Z" end="20000105T000000Z"/>
|
||||
</C:calendar-data>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:time-range start="20000101T000000Z" end="20000105T000000Z"/>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>
|
||||
"""
|
||||
status, responses = self.report("/test", request)
|
||||
assert status == 207
|
||||
assert len(responses) == 0
|
||||
|
||||
def test_report_time_range_one_vevent(self) -> None:
|
||||
"""Test calendar-query with time-range filter, matches one VEVENT."""
|
||||
self.mkcalendar("/test/")
|
||||
self.put("/test/calendar1.ics/", get_file_content("event_daily_rrule.ics"))
|
||||
self.put("/test/calendar2.ics/", get_file_content("event1.ics"))
|
||||
|
||||
start = "20060101T000000Z"
|
||||
end = "20060104T000000Z"
|
||||
|
||||
request = f"""
|
||||
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<C:calendar-data>
|
||||
<C:expand start="{start}" end="{end}"/>
|
||||
</C:calendar-data>
|
||||
</D:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:time-range start="{start}" end="{end}"/>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>
|
||||
"""
|
||||
status, responses = self.report("/test", request)
|
||||
assert status == 207
|
||||
assert len(responses) == 1
|
||||
response = responses["/test/calendar1.ics"]
|
||||
assert type(response) is dict
|
||||
status, element = response["C:calendar-data"]
|
||||
assert status == 200 and element.text
|
||||
assert "BEGIN:VEVENT" in element.text
|
||||
assert "RECURRENCE-ID:20060103T170000Z" in element.text
|
||||
assert "DTSTART:20060103T170000Z" in element.text
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue