get.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. # This file is part of Radicale - CalDAV and CardDAV server
  2. # Copyright © 2008 Nicolas Kandel
  3. # Copyright © 2008 Pascal Halter
  4. # Copyright © 2008-2017 Guillaume Ayoub
  5. # Copyright © 2017-2023 Unrud <unrud@outlook.com>
  6. # Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
  7. #
  8. # This library is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This library is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  20. import posixpath
  21. from http import client
  22. from urllib.parse import quote
  23. from radicale import httputils, pathutils, storage, types, xmlutils
  24. from radicale.app.base import Access, ApplicationBase
  25. from radicale.log import logger
  26. def propose_filename(collection: storage.BaseCollection) -> str:
  27. """Propose a filename for a collection."""
  28. if collection.tag == "VADDRESSBOOK":
  29. fallback_title = "Address book"
  30. suffix = ".vcf"
  31. elif collection.tag == "VCALENDAR":
  32. fallback_title = "Calendar"
  33. suffix = ".ics"
  34. else:
  35. fallback_title = posixpath.basename(collection.path)
  36. suffix = ""
  37. title = collection.get_meta("D:displayname") or fallback_title
  38. if title and not title.lower().endswith(suffix.lower()):
  39. title += suffix
  40. return title
  41. class ApplicationPartGet(ApplicationBase):
  42. def _content_disposition_attachment(self, filename: str) -> str:
  43. value = "attachment"
  44. try:
  45. encoded_filename = quote(filename, encoding=self._encoding)
  46. except UnicodeEncodeError:
  47. logger.warning("Failed to encode filename: %r", filename,
  48. exc_info=True)
  49. encoded_filename = ""
  50. if encoded_filename:
  51. value += "; filename*=%s''%s" % (self._encoding, encoded_filename)
  52. return value
  53. def do_GET(self, environ: types.WSGIEnviron, base_prefix: str, path: str,
  54. user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
  55. """Manage GET request."""
  56. # Redirect to /.web if the root path is requested
  57. if not pathutils.strip_path(path):
  58. return httputils.redirect(base_prefix + "/.web")
  59. if path == "/.web" or path.startswith("/.web/"):
  60. # Redirect to sanitized path for all subpaths of /.web
  61. unsafe_path = environ.get("PATH_INFO", "")
  62. if len(base_prefix) > 0:
  63. unsafe_path = unsafe_path.removeprefix(base_prefix)
  64. if unsafe_path != path:
  65. location = base_prefix + path
  66. logger.info("Redirecting to sanitized path: %r ==> %r",
  67. base_prefix + unsafe_path, location)
  68. return httputils.redirect(location, client.MOVED_PERMANENTLY)
  69. # Dispatch /.web path to web module
  70. return self._web.get(environ, base_prefix, path, user)
  71. access = Access(self._rights, user, path)
  72. if not access.check("r") and "i" not in access.permissions:
  73. return httputils.NOT_ALLOWED
  74. with self._storage.acquire_lock("r", user):
  75. item = next(iter(self._storage.discover(path)), None)
  76. if not item:
  77. return httputils.NOT_FOUND
  78. if access.check("r", item):
  79. limited_access = False
  80. elif "i" in access.permissions:
  81. limited_access = True
  82. else:
  83. return httputils.NOT_ALLOWED
  84. if isinstance(item, storage.BaseCollection):
  85. if not item.tag:
  86. return (httputils.NOT_ALLOWED if limited_access else
  87. httputils.DIRECTORY_LISTING)
  88. content_type = xmlutils.MIMETYPES[item.tag]
  89. content_disposition = self._content_disposition_attachment(
  90. propose_filename(item))
  91. elif limited_access:
  92. return httputils.NOT_ALLOWED
  93. else:
  94. content_type = xmlutils.OBJECT_MIMETYPES[item.name]
  95. content_disposition = ""
  96. assert item.last_modified
  97. headers = {
  98. "Content-Type": content_type,
  99. "Last-Modified": item.last_modified,
  100. "ETag": item.etag}
  101. if content_disposition:
  102. headers["Content-Disposition"] = content_disposition
  103. answer = item.serialize()
  104. return client.OK, headers, answer, None