Merge pull request #2035 from pbiering/sharing-fix-get-filename-propose

Sharing fix get filename propose
This commit is contained in:
Peter Bieringer
2026-03-22 08:43:50 +01:00
committed by GitHub
2 changed files with 50 additions and 8 deletions

View File

@@ -20,6 +20,7 @@
import posixpath import posixpath
from http import client from http import client
from typing import Union
from urllib.parse import quote from urllib.parse import quote
from radicale import httputils, pathutils, storage, types, xmlutils from radicale import httputils, pathutils, storage, types, xmlutils
@@ -27,18 +28,26 @@ from radicale.app.base import Access, ApplicationBase
from radicale.log import logger 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.""" """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" fallback_title = "Address book"
suffix = ".vcf" suffix = ".vcf"
elif collection.tag == "VCALENDAR": elif collection.tag == "VCALENDAR" or share_bday_automap:
fallback_title = "Calendar" fallback_title = "Calendar"
suffix = ".ics" suffix = ".ics"
else: else:
fallback_title = posixpath.basename(collection.path) fallback_title = posixpath.basename(collection.path)
suffix = "" 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()): if title and not title.lower().endswith(suffix.lower()):
title += suffix title += suffix
return title return title
@@ -77,6 +86,7 @@ class ApplicationPartGet(ApplicationBase):
# Dispatch /.web path to web module # Dispatch /.web path to web module
return self._web.get(environ, base_prefix, path, user) return self._web.get(environ, base_prefix, path, user)
permissions_filter = None permissions_filter = None
share = None
if self._sharing._enabled: if self._sharing._enabled:
# Sharing by token or map (if enabled) # Sharing by token or map (if enabled)
share = self._sharing.sharing_collection_resolver(path, user) share = self._sharing.sharing_collection_resolver(path, user)
@@ -102,9 +112,12 @@ class ApplicationPartGet(ApplicationBase):
if not item.tag: if not item.tag:
return (httputils.NOT_ALLOWED if limited_access else return (httputils.NOT_ALLOWED if limited_access else
httputils.DIRECTORY_LISTING) 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( content_disposition = self._content_disposition_attachment(
propose_filename(item)) propose_filename(item, share))
elif limited_access: elif limited_access:
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
else: else:

View File

@@ -903,6 +903,8 @@ class TestSharingApiSanity(BaseTest):
"collection_by_map": "True", "collection_by_map": "True",
"collection_by_token": "True"}, "collection_by_token": "True"},
"logging": {"request_header_on_debug": "False", "logging": {"request_header_on_debug": "False",
"response_header_on_debug": "True",
"response_content_on_debug": "True",
"request_content_on_debug": "False"}, "request_content_on_debug": "False"},
"rights": {"type": "owner_only"}}) "rights": {"type": "owner_only"}})
@@ -983,6 +985,9 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** fetch collection (with credentials) as owner") logging.info("\n*** fetch collection (with credentials) as owner")
_, headers, answer = self.request("GET", path_mapped, check=200, login="owner:ownerpw") _, headers, answer = self.request("GET", path_mapped, check=200, login="owner:ownerpw")
assert "UID:event" in answer 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") logging.info("\n*** fetch item (with credentials) as owner")
_, headers, answer = self.request("GET", path_mapped_item1, check=200, login="owner:ownerpw") _, 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") _, headers, answer = self.request("GET", path_shared, check=200, login="user:userpw")
assert "UID:event1" in answer assert "UID:event1" in answer
assert "UID:event2" 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") logging.info("\n*** fetch item via map (with credentials) as user")
_, headers, answer = self.request("GET", path_shared_item1, check=200, login="user:userpw") _, headers, answer = self.request("GET", path_shared_item1, check=200, login="user:userpw")
@@ -4197,6 +4205,7 @@ permissions: RrWw""")
"collection_by_bday": "True"}, "collection_by_bday": "True"},
"logging": {"request_header_on_debug": "False", "logging": {"request_header_on_debug": "False",
"response_content_on_debug": "True", "response_content_on_debug": "True",
"response_header_on_debug": "True",
"request_content_on_debug": "True"}, "request_content_on_debug": "True"},
"rights": {"type": "owner_only"}}) "rights": {"type": "owner_only"}})
@@ -4238,9 +4247,12 @@ permissions: RrWw""")
# execute GET as owner # execute GET as owner
logging.info("\n*** GET VCF collection owner -> ok") 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 "contact1" in answer
assert "contact2" in answer assert "contact2" in answer
# title from fallback
assert 'Content-Disposition' in headers
assert 'Address%20book.vcf' in headers['Content-Disposition']
# create map # create map
logging.info("\n*** create bday user/owner:r -> ok") logging.info("\n*** create bday user/owner:r -> ok")
@@ -4251,6 +4263,7 @@ permissions: RrWw""")
json_dict['Permissions'] = "r" json_dict['Permissions'] = "r"
json_dict['Enabled'] = True json_dict['Enabled'] = True
json_dict['Hidden'] = False 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) _, headers, answer = self._sharing_api_json("bday", "create", check=200, login="owner:ownerpw", json_dict=json_dict)
answer_dict = json.loads(answer) answer_dict = json.loads(answer)
assert answer_dict['Status'] == "success" assert answer_dict['Status'] == "success"
@@ -4283,7 +4296,7 @@ permissions: RrWw""")
# verify content as user # verify content as user
logging.info("\n*** GET collection user -> ok") 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:VCARD" not in answer
assert "BEGIN:VCALENDAR" in answer assert "BEGIN:VCALENDAR" in answer
assert "RRULE:FREQ=YEARLY" in answer assert "RRULE:FREQ=YEARLY" in answer
@@ -4291,6 +4304,12 @@ permissions: RrWw""")
assert "DTEND;VALUE=DATE:19700102" in answer assert "DTEND;VALUE=DATE:19700102" in answer
assert "TRANSP:TRANSPARENT" in answer assert "TRANSP:TRANSPARENT" in answer
assert "DESCRIPTION:BDAY=1970-01-01" 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 # verify report as user
logging.info("\n*** REPORT collection user -> ok") logging.info("\n*** REPORT collection user -> ok")
@@ -4549,6 +4568,7 @@ permissions: RrWw""")
"enforce_properties_overlay": "True", "enforce_properties_overlay": "True",
"collection_by_bday": "True"}, "collection_by_bday": "True"},
"logging": {"request_header_on_debug": "False", "logging": {"request_header_on_debug": "False",
"response_header_on_debug": "True",
"response_content_on_debug": "True", "response_content_on_debug": "True",
"request_content_on_debug": "True"}, "request_content_on_debug": "True"},
"rights": {"type": "owner_only"}}) "rights": {"type": "owner_only"}})
@@ -4674,3 +4694,12 @@ permissions: RrWw""")
assert "D:sync-token" not in response assert "D:sync-token" not in response
assert "C:supported-calendar-component-set" in response assert "C:supported-calendar-component-set" in response
assert "D:current-user-privilege-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']