get.py 4.7 KB

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