diff --git a/radicale/app/get.py b/radicale/app/get.py index e42f27ff..9accb8ce 100644 --- a/radicale/app/get.py +++ b/radicale/app/get.py @@ -20,6 +20,7 @@ import posixpath from http import client +from typing import Union from urllib.parse import quote from radicale import httputils, pathutils, storage, types, xmlutils @@ -27,18 +28,26 @@ from radicale.app.base import Access, ApplicationBase from radicale.log import logger -def propose_filename(collection: storage.BaseCollection) -> str: +def propose_filename(collection: storage.BaseCollection, share: Union[dict, None] = None) -> str: """Propose a filename for a collection.""" - if collection.tag == "VADDRESSBOOK": + share_bday_automap = False + if share and share['ShareType'] == "bday": + share_bday_automap = True + if collection.tag == "VADDRESSBOOK" and not share_bday_automap: fallback_title = "Address book" suffix = ".vcf" - elif collection.tag == "VCALENDAR": + elif collection.tag == "VCALENDAR" or share_bday_automap: fallback_title = "Calendar" suffix = ".ics" else: fallback_title = posixpath.basename(collection.path) suffix = "" - title = collection.get_meta("D:displayname") or fallback_title + if share and 'Properties' in share and share['Properties'] and "D:displayname" in share['Properties']: + title = share['Properties']["D:displayname"] + elif share_bday_automap: + title = fallback_title + else: + title = collection.get_meta("D:displayname") or fallback_title if title and not title.lower().endswith(suffix.lower()): title += suffix return title @@ -77,6 +86,7 @@ class ApplicationPartGet(ApplicationBase): # Dispatch /.web path to web module return self._web.get(environ, base_prefix, path, user) permissions_filter = None + share = None if self._sharing._enabled: # Sharing by token or map (if enabled) share = self._sharing.sharing_collection_resolver(path, user) @@ -102,9 +112,12 @@ class ApplicationPartGet(ApplicationBase): if not item.tag: return (httputils.NOT_ALLOWED if limited_access else httputils.DIRECTORY_LISTING) - content_type = xmlutils.MIMETYPES[item.tag] + if share and share['ShareType'] == "bday": + content_type = xmlutils.MIMETYPES["VCALENDAR"] + else: + content_type = xmlutils.MIMETYPES[item.tag] content_disposition = self._content_disposition_attachment( - propose_filename(item)) + propose_filename(item, share)) elif limited_access: return httputils.NOT_ALLOWED else: diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index b1d5cc21..e46ac795 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -903,6 +903,8 @@ class TestSharingApiSanity(BaseTest): "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", + "response_header_on_debug": "True", + "response_content_on_debug": "True", "request_content_on_debug": "False"}, "rights": {"type": "owner_only"}}) @@ -983,6 +985,9 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** fetch collection (with credentials) as owner") _, headers, answer = self.request("GET", path_mapped, check=200, login="owner:ownerpw") assert "UID:event" in answer + assert 'Content-Disposition' in headers + # fallback title + assert 'Calendar.ics' in headers['Content-Disposition'] logging.info("\n*** fetch item (with credentials) as owner") _, headers, answer = self.request("GET", path_mapped_item1, check=200, login="owner:ownerpw") @@ -995,6 +1000,9 @@ class TestSharingApiSanity(BaseTest): _, headers, answer = self.request("GET", path_shared, check=200, login="user:userpw") assert "UID:event1" in answer assert "UID:event2" in answer + assert 'Content-Disposition' in headers + # title from Properties + assert 'Test.ics' in headers['Content-Disposition'] logging.info("\n*** fetch item via map (with credentials) as user") _, headers, answer = self.request("GET", path_shared_item1, check=200, login="user:userpw") @@ -4197,6 +4205,7 @@ permissions: RrWw""") "collection_by_bday": "True"}, "logging": {"request_header_on_debug": "False", "response_content_on_debug": "True", + "response_header_on_debug": "True", "request_content_on_debug": "True"}, "rights": {"type": "owner_only"}}) @@ -4238,9 +4247,12 @@ permissions: RrWw""") # execute GET as owner logging.info("\n*** GET VCF collection owner -> ok") - _, answer = self.get(path_mapped, login="owner:ownerpw") + _, headers, answer = self.request("GET", path_mapped, login="owner:ownerpw") assert "contact1" in answer assert "contact2" in answer + # title from fallback + assert 'Content-Disposition' in headers + assert 'Address%20book.vcf' in headers['Content-Disposition'] # create map logging.info("\n*** create bday user/owner:r -> ok") @@ -4251,6 +4263,7 @@ permissions: RrWw""") json_dict['Permissions'] = "r" json_dict['Enabled'] = True json_dict['Hidden'] = False + json_dict['Properties'] = {"D:displayname": "Test-BDAY"} _, headers, answer = self._sharing_api_json("bday", "create", check=200, login="owner:ownerpw", json_dict=json_dict) answer_dict = json.loads(answer) assert answer_dict['Status'] == "success" @@ -4283,7 +4296,7 @@ permissions: RrWw""") # verify content as user logging.info("\n*** GET collection user -> ok") - _, answer = self.get(path_shared_r, login="user:userpw") + _, headers, answer = self.request("GET", path_shared_r, login="user:userpw") assert "BEGIN:VCARD" not in answer assert "BEGIN:VCALENDAR" in answer assert "RRULE:FREQ=YEARLY" in answer @@ -4291,6 +4304,12 @@ permissions: RrWw""") assert "DTEND;VALUE=DATE:19700102" in answer assert "TRANSP:TRANSPARENT" in answer assert "DESCRIPTION:BDAY=1970-01-01" in answer + # content type must be adjusted + assert 'Content-Type' in headers + assert 'text/calendar' in headers['Content-Type'] + # title from Properties + assert 'Content-Disposition' in headers + assert 'Test-BDAY.ics' in headers['Content-Disposition'] # verify report as user logging.info("\n*** REPORT collection user -> ok") @@ -4549,6 +4568,7 @@ permissions: RrWw""") "enforce_properties_overlay": "True", "collection_by_bday": "True"}, "logging": {"request_header_on_debug": "False", + "response_header_on_debug": "True", "response_content_on_debug": "True", "request_content_on_debug": "True"}, "rights": {"type": "owner_only"}}) @@ -4674,3 +4694,12 @@ permissions: RrWw""") assert "D:sync-token" not in response assert "C:supported-calendar-component-set" in response assert "D:current-user-privilege-set" in response + + # verify content as owner + logging.info("\n*** GET collection owner -> ok") + _, headers, answer = self.request("GET", path_shared, login="owner:ownerpw") + assert 'Content-Type' in headers + assert 'text/calendar' in headers['Content-Type'] + # title from default + assert 'Content-Disposition' in headers + assert 'Calendar.ics' in headers['Content-Disposition']