caldav report implementation

merge-requests/30/head
Bob Mottram 2022-02-23 18:04:34 +00:00
parent ef8c7a5eb9
commit 26610e4555
2 changed files with 249 additions and 86 deletions

View File

@ -16906,7 +16906,8 @@ class PubServer(BaseHTTPRequestHandler):
nickname, self.server.domain,
depth, propfind_xml,
self.server.http_prefix,
self.server.system_language)
self.server.system_language,
self.server.recent_dav_etags)
elif endpoint_type == 'report':
curr_etag = None
if self.headers.get('ETag'):
@ -16920,6 +16921,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.person_cache,
self.server.http_prefix,
curr_etag,
self.server.recent_dav_etags,
self.server.domain_full)
elif endpoint_type == 'delete':
response_str = \
@ -16933,7 +16935,10 @@ class PubServer(BaseHTTPRequestHandler):
self._404()
return
if response_str == 'Not modified':
return self._304()
if endpoint_type == 'put':
return self._200()
else:
return self._304()
elif response_str.startswith('ETag:') and endpoint_type == 'put':
response_etag = response_str.split('ETag:', 1)[1]
self._201(response_etag)
@ -19282,6 +19287,9 @@ def run_daemon(dyslexic_font: bool,
default_reply_interval_hrs = 9999999
httpd.default_reply_interval_hrs = default_reply_interval_hrs
# recent caldav etags for each account
httpd.recent_dav_etags = {}
httpd.key_shortcuts = {}
load_access_keys_for_accounts(base_dir, httpd.key_shortcuts,
httpd.access_keys)

View File

@ -27,6 +27,31 @@ from filters import is_filtered
from context import get_individual_post_context
def _dav_date_from_string(timestamp: str) -> str:
"""Returns a datetime from a caldav date
"""
timestamp_year = timestamp[:4]
timestamp_month = timestamp[4:][:2]
timestamp_day = timestamp[6:][:2]
timestamp_hour = timestamp[9:][:2]
timestamp_min = timestamp[11:][:2]
timestamp_sec = timestamp[13:][:2]
if not timestamp_year.isdigit() or \
not timestamp_month.isdigit() or \
not timestamp_day.isdigit() or \
not timestamp_hour.isdigit() or \
not timestamp_min.isdigit() or \
not timestamp_sec.isdigit():
return None
if int(timestamp_year) < 2020 or int(timestamp_year) > 2100:
return None
published = \
timestamp_year + '-' + timestamp_month + '-' + timestamp_day + 'T' + \
timestamp_hour + ':' + timestamp_min + ':' + timestamp_sec + 'Z'
return published
def _valid_uuid(test_uuid: str, version: int):
"""Check if uuid_to_test is a valid UUID
"""
@ -784,66 +809,15 @@ def _dav_store_event(base_dir: str, nickname: str, domain: str,
return False
# convert to the expected time format
timestamp_year = timestamp[:4]
timestamp_month = timestamp[4:][:2]
timestamp_day = timestamp[6:][:2]
timestamp_hour = timestamp[9:][:2]
timestamp_min = timestamp[11:][:2]
timestamp_sec = timestamp[13:][:2]
if not timestamp_year.isdigit() or \
not timestamp_month.isdigit() or \
not timestamp_day.isdigit() or \
not timestamp_hour.isdigit() or \
not timestamp_min.isdigit() or \
not timestamp_sec.isdigit():
published = _dav_date_from_string(timestamp)
if not published:
return False
if int(timestamp_year) < 2020 or int(timestamp_year) > 2100:
start_time = _dav_date_from_string(start_time)
if not start_time:
return False
published = \
timestamp_year + '-' + timestamp_month + '-' + timestamp_day + 'T' + \
timestamp_hour + ':' + timestamp_min + ':' + timestamp_sec + 'Z'
start_time_year = start_time[:4]
start_time_month = start_time[4:][:2]
start_time_day = start_time[6:][:2]
start_time_hour = start_time[9:][:2]
start_time_min = start_time[11:][:2]
start_time_sec = start_time[13:][:2]
if not start_time_year.isdigit() or \
not start_time_month.isdigit() or \
not start_time_day.isdigit() or \
not start_time_hour.isdigit() or \
not start_time_min.isdigit() or \
not start_time_sec.isdigit():
end_time = _dav_date_from_string(end_time)
if not end_time:
return False
if int(start_time_year) < 2020 or int(start_time_year) > 2100:
return False
start_time = \
start_time_year + '-' + start_time_month + '-' + \
start_time_day + 'T' + \
start_time_hour + ':' + start_time_min + ':' + start_time_sec + 'Z'
end_time_year = end_time[:4]
end_time_month = end_time[4:][:2]
end_time_day = end_time[6:][:2]
end_time_hour = end_time[9:][:2]
end_time_min = end_time[11:][:2]
end_time_sec = end_time[13:][:2]
if not end_time_year.isdigit() or \
not end_time_month.isdigit() or \
not end_time_day.isdigit() or \
not end_time_hour.isdigit() or \
not end_time_min.isdigit() or \
not end_time_sec.isdigit():
return False
if int(end_time_year) < 2020 or int(end_time_year) > 2100:
return False
end_time = \
end_time_year + '-' + end_time_month + '-' + end_time_day + 'T' + \
end_time_hour + ':' + end_time_min + ':' + end_time_sec + 'Z'
post_id = ''
post_context = get_individual_post_context()
@ -932,9 +906,26 @@ def _dav_store_event(base_dir: str, nickname: str, domain: str,
return True
def _dav_update_recent_etags(etag: str, nickname: str,
recent_dav_etags: {}) -> None:
"""Updates the recent etags for each account
"""
# update the recent caldav etags for each account
if not recent_dav_etags.get(nickname):
recent_dav_etags[nickname] = [etag]
else:
# only keep a limited number of recent etags
while len(recent_dav_etags[nickname]) > 32:
recent_dav_etags[nickname].pop(0)
# append the etag to the recent list
if etag not in recent_dav_etags[nickname]:
recent_dav_etags[nickname].append(etag)
def dav_put_response(base_dir: str, nickname: str, domain: str,
depth: int, xml_str: str, http_prefix: str,
system_language: str) -> str:
system_language: str,
recent_dav_etags: {}) -> str:
"""Returns the response to caldav PUT
"""
if '\n' not in xml_str:
@ -946,6 +937,11 @@ def dav_put_response(base_dir: str, nickname: str, domain: str,
'END:VEVENT' not in xml_str:
return None
etag = md5(xml_str).hexdigest()
if recent_dav_etags.get(nickname):
if etag in recent_dav_etags[nickname]:
return 'Not modified'
stored_count = 0
reading_event = False
lines_list = xml_str.split('\n')
@ -968,13 +964,14 @@ def dav_put_response(base_dir: str, nickname: str, domain: str,
event_list.append(line)
if stored_count == 0:
return None
return 'ETag:' + md5(xml_str).hexdigest()
_dav_update_recent_etags(etag, nickname, recent_dav_etags)
return 'ETag:' + etag
def dav_report_response(base_dir: str, nickname: str, domain: str,
depth: int, xml_str: str,
person_cache: {}, http_prefix: str,
curr_etag: str,
curr_etag: str, recent_dav_etags: {},
domain_full: str) -> str:
"""Returns the response to caldav REPORT
"""
@ -983,42 +980,200 @@ def dav_report_response(base_dir: str, nickname: str, domain: str,
if '<c:calendar-multiget' not in xml_str or \
'</c:calendar-multiget>' not in xml_str:
return None
if curr_etag:
if recent_dav_etags.get(nickname):
if curr_etag in recent_dav_etags[nickname]:
return "Not modified"
xml_str_lower = xml_str.lower()
query_start_time = None
query_end_time = None
if ':time-range' in xml_str_lower:
time_range_str = xml_str_lower.split(':time-range')[1]
if 'start=' in time_range_str and 'end=' in time_range_str:
start_time_str = time_range_str.split('start=')[1]
if start_time_str.startswith("'"):
query_start_time_str = start_time_str.split("'")[1]
query_start_time = _dav_date_from_string(query_start_time_str)
elif start_time_str.startswith('"'):
query_start_time_str = start_time_str.split('"')[1]
query_start_time = _dav_date_from_string(query_start_time_str)
end_time_str = time_range_str.split('end=')[1]
if end_time_str.startswith("'"):
query_end_time_str = end_time_str.split("'")[1]
query_end_time = _dav_date_from_string(query_end_time_str)
elif end_time_str.startswith('"'):
query_end_time_str = end_time_str.split('"')[1]
query_end_time = _dav_date_from_string(query_end_time_str)
ical_events = None
etag = None
events_href = ''
responses = ''
search_date = datetime.now()
if query_start_time and query_end_time:
query_start_year = int(query_start_time.split('-')[0])
query_start_month = int(query_start_time.split('-')[1])
query_start_day = query_start_time.split('-')[2]
query_start_day = int(query_start_day.split('T')[0])
query_end_year = int(query_end_time.split('-')[0])
query_end_month = int(query_end_time.split('-')[1])
query_end_day = query_end_time.split('-')[2]
query_end_day = int(query_end_day.split('T')[0])
if query_start_year == query_end_year and \
query_start_month == query_end_month:
if query_start_day == query_end_day:
# calendar for one day
search_date = \
datetime.datetime(year=query_start_year,
month=query_start_month,
day=query_start_day)
ical_events = \
get_todays_events_icalendar(base_dir, nickname, domain,
search_date.year,
search_date.month,
search_date.day, person_cache,
http_prefix)
events_href = \
http_prefix + '://' + domain_full + '/users/' + \
nickname + '/calendar?year=' + \
str(search_date.year) + '?month=' + \
str(search_date.month) + '?day=' + str(search_date.day)
if ical_events:
etag = md5(ical_events).hexdigest()
responses = \
' <d:response>\n' + \
' <d:href>' + events_href + '</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + \
etag + '"</d:getetag>\n' + \
' <c:calendar-data>' + ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK' + \
'</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\n'
elif query_start_day == 1 and query_start_day >= 28:
# calendar for a month
ical_events = \
get_month_events_icalendar(base_dir, nickname, domain,
query_start_year,
query_start_month,
person_cache,
http_prefix)
events_href = \
http_prefix + '://' + domain_full + '/users/' + \
nickname + '/calendar?year=' + \
str(query_start_year) + '?month=' + \
str(query_start_month)
if ical_events:
etag = md5(ical_events).hexdigest()
responses = \
' <d:response>\n' + \
' <d:href>' + events_href + '</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + \
etag + '"</d:getetag>\n' + \
' <c:calendar-data>' + ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK' + \
'</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\n'
if not responses:
all_events = ''
for year in range(query_start_year, query_end_year+1):
if query_start_year == query_end_year:
start_month_number = query_start_month
end_month_number = query_end_month
elif year == query_end_year:
start_month_number = 1
end_month_number = query_end_month
elif year == query_start_year:
start_month_number = query_start_month
end_month_number = 12
else:
start_month_number = 1
end_month_number = 12
for month in range(start_month_number, end_month_number+1):
ical_events = \
get_month_events_icalendar(base_dir,
nickname, domain,
year, month,
person_cache,
http_prefix)
events_href = \
http_prefix + '://' + domain_full + '/users/' + \
nickname + '/calendar?year=' + \
str(year) + '?month=' + \
str(month)
if ical_events:
all_events += ical_events
responses += \
' <d:response>\n' + \
' <d:href>' + events_href + \
'</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + \
etag + '"</d:getetag>\n' + \
' <c:calendar-data>' + \
ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK' + \
'</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\n'
etag = md5(all_events).hexdigest()
# today's calendar events
now = datetime.now()
ical_events = \
get_todays_events_icalendar(base_dir, nickname, domain,
now.year, now.month,
now.day, person_cache,
http_prefix)
if not ical_events:
ical_events = \
get_todays_events_icalendar(base_dir, nickname, domain,
search_date.year, search_date.month,
search_date.day, person_cache,
http_prefix)
events_href = \
http_prefix + '://' + domain_full + '/users/' + \
nickname + '/calendar?year=' + \
str(search_date.year) + '?month=' + \
str(search_date.month) + '?day=' + str(search_date.day)
if ical_events:
etag = md5(ical_events).hexdigest()
responses = \
' <d:response>\n' + \
' <d:href>' + events_href + '</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + etag + '"</d:getetag>\n' + \
' <c:calendar-data>' + ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\n'
if not ical_events or not etag:
return None
if 'VEVENT' not in ical_events:
return None
etag = md5(ical_events).hexdigest()
if etag == curr_etag:
return "Not modified"
events_href = \
http_prefix + '://' + domain_full + '/users/' + \
nickname + '/calendar?year=' + \
str(now.year) + '?month=' + str(now.month) + '?day=' + str(now.day)
response_str = \
'<?xml version="1.0" encoding="utf-8" ?>\n' + \
'<d:multistatus xmlns:d="DAV:" ' + \
'xmlns:cs="http://calendarserver.org/ns/">\n' + \
' <d:response>\n' + \
' <d:href>' + events_href + '</d:href>\n' + \
' <d:propstat>\n' + \
' <d:prop>\n' + \
' <d:getetag>"' + etag + '"</d:getetag>\n' + \
' <c:calendar-data>' + ical_events + \
' </c:calendar-data>\n' + \
' </d:prop>\n' + \
' <d:status>HTTP/1.1 200 OK</d:status>\n' + \
' </d:propstat>\n' + \
' </d:response>\n' + \
' <d:response>\n' + \
'</d:multistatus>'
responses + '</d:multistatus>'
_dav_update_recent_etags(etag, nickname, recent_dav_etags)
return response_str