Merge pull request #2190 from pbiering/issue-2188
max options extension
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
## 3.7.8.dev
|
||||
* 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
|
||||
|
||||
@@ -866,6 +866,19 @@ Limited to 80% of max_content_length to cover plain base64 encoded payload.
|
||||
|
||||
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 @@ This is an automated message. Please do not reply.
|
||||
|
||||
#### [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)_
|
||||
|
||||
7
config
7
config
@@ -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 @@ Content-Security-Policy = default-src 'self'; object-src 'none'
|
||||
|
||||
[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
|
||||
|
||||
@@ -200,9 +200,10 @@ def run() -> None:
|
||||
|
||||
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:
|
||||
|
||||
@@ -79,6 +79,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
||||
_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 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
||||
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")
|
||||
|
||||
@@ -122,6 +122,7 @@ class ApplicationBase:
|
||||
_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
|
||||
|
||||
@@ -47,6 +47,7 @@ PRODID = u"-//Radicale//NONSGML Version " + utils.package_version("radicale") +
|
||||
|
||||
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 @@ def prepare(vobject_items: List[vobject.base.Component], path: str,
|
||||
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 @@ class ApplicationPartPut(ApplicationBase):
|
||||
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 @@ class ApplicationPartPut(ApplicationBase):
|
||||
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:
|
||||
|
||||
@@ -895,9 +895,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,
|
||||
@@ -909,6 +909,7 @@ class ApplicationPartReport(ApplicationBase):
|
||||
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,
|
||||
|
||||
@@ -254,6 +254,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
||||
"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 @@ This is an automated message. Please do not reply.""",
|
||||
("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})]))
|
||||
])
|
||||
|
||||
|
||||
@@ -53,6 +53,17 @@ VCF_TO_ICS_SUPPORTED_PLACEHOLDERS: list = ["fn", "n:f", "n:g", "n:a", "age", "ni
|
||||
# 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 predict_tag_of_whole_collection(
|
||||
|
||||
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 @@ def check_and_sanitize_items(
|
||||
if ref_value_param is not None:
|
||||
dates.params["VALUE"] = ref_value_param
|
||||
# vobject interprets recurrence rules on demand
|
||||
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:
|
||||
component.rruleset
|
||||
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 @@ def find_time_range(vobject_item: vobject.base.Component, tag: str
|
||||
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 @@ def verify(file: str, encoding: str):
|
||||
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:
|
||||
|
||||
@@ -106,6 +106,7 @@ class StorageBase(storage.BaseStorage):
|
||||
"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")
|
||||
|
||||
@@ -124,7 +124,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
|
||||
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)
|
||||
|
||||
35
radicale/tests/static/event1_y2040.ics
Normal file
35
radicale/tests/static/event1_y2040.ics
Normal file
@@ -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:unclesam@example.com
|
||||
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Jane Doe:MAILTO:janedoe@example.com
|
||||
ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="MAILTO:bob@host.com";PARTSTAT=ACCEPTED;CN=John Doe:MAILTO:johndoe@example.com
|
||||
DTSTART;TZID=Europe/Paris:20400901T180000
|
||||
DTEND;TZID=Europe/Paris:20400901T190000
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
31
radicale/tests/static/event_full_day_rrule_count_500.ics
Normal file
31
radicale/tests/static/event_full_day_rrule_count_500.ics
Normal file
@@ -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
|
||||
|
||||
31
radicale/tests/static/event_full_day_rrule_until_2y.ics
Normal file
31
radicale/tests/static/event_full_day_rrule_until_2y.ics
Normal file
@@ -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
|
||||
|
||||
31
radicale/tests/static/event_full_day_rrule_until_5000y.ics
Normal file
31
radicale/tests/static/event_full_day_rrule_until_5000y.ics
Normal file
@@ -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
|
||||
|
||||
31
radicale/tests/static/event_full_day_rrule_until_50y.ics
Normal file
31
radicale/tests/static/event_full_day_rrule_until_50y.ics
Normal file
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -151,6 +151,19 @@ permissions: RrWw""")
|
||||
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 @@ permissions: RrWw""")
|
||||
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/")
|
||||
|
||||
@@ -128,7 +128,8 @@ permissions: RrWw""")
|
||||
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 @@ permissions: RrWw""")
|
||||
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,15 +179,19 @@ permissions: RrWw""")
|
||||
uids.append(line)
|
||||
|
||||
if line.startswith("RECURRENCE-ID:"):
|
||||
if expected_recurrence_ids:
|
||||
assert line in expected_recurrence_ids
|
||||
recurrence_ids.append(line)
|
||||
|
||||
if line.startswith("DTSTART:"):
|
||||
if expected_start_times:
|
||||
assert line in expected_start_times
|
||||
|
||||
if line.startswith("DTEND:"):
|
||||
if expected_end_times:
|
||||
assert line in expected_end_times
|
||||
|
||||
if expected_recurrence_ids:
|
||||
assert len(uids) == len(expected_recurrence_ids)
|
||||
assert len(set(recurrence_ids)) == len(expected_recurrence_ids)
|
||||
|
||||
@@ -289,6 +296,35 @@ permissions: RrWw""")
|
||||
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 @@ permissions: RrWw""")
|
||||
|
||||
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 @@ permissions: RrWw""")
|
||||
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user