From 26610e455521b8f340626dbdfcb86b9a0d2cf919 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 23 Feb 2022 18:04:34 +0000 Subject: [PATCH] caldav report implementation --- daemon.py | 12 +- happening.py | 323 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 249 insertions(+), 86 deletions(-) diff --git a/daemon.py b/daemon.py index 5ae348e8a..b0b83d708 100644 --- a/daemon.py +++ b/daemon.py @@ -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) diff --git a/happening.py b/happening.py index c68ff03c7..22bff046f 100644 --- a/happening.py +++ b/happening.py @@ -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 '' 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 = \ + ' \n' + \ + ' ' + events_href + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + \ + etag + '"\n' + \ + ' ' + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK' + \ + '\n' + \ + ' \n' + \ + ' \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 = \ + ' \n' + \ + ' ' + events_href + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + \ + etag + '"\n' + \ + ' ' + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK' + \ + '\n' + \ + ' \n' + \ + ' \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 += \ + ' \n' + \ + ' ' + events_href + \ + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + \ + etag + '"\n' + \ + ' ' + \ + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK' + \ + '\n' + \ + ' \n' + \ + ' \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 = \ + ' \n' + \ + ' ' + events_href + '\n' + \ + ' \n' + \ + ' \n' + \ + ' "' + etag + '"\n' + \ + ' ' + ical_events + \ + ' \n' + \ + ' \n' + \ + ' HTTP/1.1 200 OK\n' + \ + ' \n' + \ + ' \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 = \ + '\n' + \ '\n' + \ - ' \n' + \ - ' ' + events_href + '\n' + \ - ' \n' + \ - ' \n' + \ - ' "' + etag + '"\n' + \ - ' ' + ical_events + \ - ' \n' + \ - ' \n' + \ - ' HTTP/1.1 200 OK\n' + \ - ' \n' + \ - ' \n' + \ - ' \n' + \ - '' + responses + '' + _dav_update_recent_etags(etag, nickname, recent_dav_etags) return response_str