Merge pull request #2035 from pbiering/sharing-fix-get-filename-propose
Sharing fix get filename propose
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
Reference in New Issue
Block a user