Files
Radicale/radicale/app/get.py

149 lines
6.5 KiB
Python

# This file is part of Radicale - CalDAV and CardDAV server
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2023 Unrud <unrud@outlook.com>
# Copyright © 2025-2026 Peter Bieringer <pb@bieringer.de>
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
import posixpath
from http import client
from typing import Union
from urllib.parse import quote
from radicale import httputils, pathutils, storage, types, xmlutils
from radicale.app.base import Access, ApplicationBase
from radicale.log import logger
def propose_filename(collection: storage.BaseCollection, share: Union[dict, None] = None) -> str:
"""Propose a filename for a collection."""
share_bday_automap = False
if share and share['Conversion'] == "bday":
share_bday_automap = True
if collection.tag == "VADDRESSBOOK" and not share_bday_automap:
fallback_title = "Address book"
suffix = ".vcf"
elif collection.tag == "VCALENDAR" or share_bday_automap:
fallback_title = "Calendar"
suffix = ".ics"
else:
fallback_title = posixpath.basename(collection.path)
suffix = ""
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
class ApplicationPartGet(ApplicationBase):
def _content_disposition_attachment(self, filename: str) -> str:
value = "attachment"
try:
encoded_filename = quote(filename, encoding=self._encoding)
except UnicodeEncodeError:
logger.warning("Failed to encode filename: %r", filename,
exc_info=True)
encoded_filename = ""
if encoded_filename:
value += "; filename*=%s''%s" % (self._encoding, encoded_filename)
return value
def do_GET(self, environ: types.WSGIEnviron, base_prefix: str, path: str,
user: str, request_info: dict) -> types.WSGIResponse:
"""Manage GET request."""
# Redirect to /.web if the root path is requested
if not pathutils.strip_path(path):
return httputils.redirect(base_prefix + "/.web")
if path == "/.web" or path.startswith("/.web/"):
# Redirect to sanitized path for all subpaths of /.web
unsafe_path = environ.get("PATH_INFO", "")
if len(base_prefix) > 0:
unsafe_path = unsafe_path.removeprefix(base_prefix)
if unsafe_path != path:
location = base_prefix + path
logger.info("Redirecting to sanitized path: %r ==> %r",
base_prefix + unsafe_path, location)
return httputils.redirect(location, client.MOVED_PERMANENTLY)
# Dispatch /.web path to web module
return self._web.get(environ, base_prefix, path, user, request_info)
permissions_filter = None
share = None
if self._sharing._enabled:
# Sharing by token or map (if enabled)
share = self._sharing.sharing_collection_resolver(path, user)
if share:
# overwrite and run through extended permission check
path = share['PathMapped']
user = share['Owner']
permissions_filter = share['Permissions']
access = Access(self._rights, user, path, permissions_filter)
if not access.check("r") and "i" not in access.permissions:
return httputils.NOT_ALLOWED
with self._storage.acquire_lock("r", user):
item = next(iter(self._storage.discover(path)), None)
if not item:
return httputils.NOT_FOUND
if access.check("r", item):
limited_access = False
elif "i" in access.permissions:
limited_access = True
else:
return httputils.NOT_ALLOWED
if isinstance(item, storage.BaseCollection):
if not item.tag:
return (httputils.NOT_ALLOWED if limited_access else
httputils.DIRECTORY_LISTING)
if share and share['Conversion'] == "bday":
content_type = xmlutils.MIMETYPES["VCALENDAR"]
else:
content_type = xmlutils.MIMETYPES[item.tag]
content_disposition = self._content_disposition_attachment(
propose_filename(item, share))
elif limited_access:
return httputils.NOT_ALLOWED
else:
if share and share['Conversion'] == "bday":
content_type = xmlutils.MIMETYPES["VCALENDAR"]
else:
content_type = xmlutils.OBJECT_MIMETYPES[item.name]
content_disposition = ""
assert item.last_modified
headers = {
"Content-Type": content_type,
"Last-Modified": item.last_modified,
"ETag": item.etag}
if content_disposition:
headers["Content-Disposition"] = content_disposition
if share and share['Conversion'] == "bday":
if isinstance(item, storage.BaseCollection):
# convert VCF to ICS
answer = item.serialize(vcf_to_ics=True, ShareActions=share['Actions'])
else:
item_converted = item.convert_vcf_to_ics(ShareActions=share['Actions'])
if item_converted is not None:
answer = item_converted.serialize()
else:
return httputils.NOT_FOUND
else:
answer = item.serialize()
return client.OK, headers, answer, None