put.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. # This file is part of Radicale Server - Calendar 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. """
  20. Radicale WSGI application.
  21. Can be used with an external WSGI server or the built-in server.
  22. """
  23. import itertools
  24. import posixpath
  25. import socket
  26. import sys
  27. from http import client
  28. import vobject
  29. from radicale import httputils
  30. from radicale import item as radicale_item
  31. from radicale import pathutils, storage, xmlutils
  32. from radicale.log import logger
  33. class ApplicationPutMixin:
  34. def do_PUT(self, environ, base_prefix, path, user):
  35. """Manage PUT request."""
  36. if not self.access(user, path, "w"):
  37. return httputils.NOT_ALLOWED
  38. try:
  39. content = self.read_content(environ)
  40. except RuntimeError as e:
  41. logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True)
  42. return httputils.BAD_REQUEST
  43. except socket.timeout as e:
  44. logger.debug("client timed out", exc_info=True)
  45. return httputils.REQUEST_TIMEOUT
  46. # Prepare before locking
  47. parent_path = pathutils.unstrip_path(
  48. posixpath.dirname(pathutils.strip_path(path)), True)
  49. permissions = self.Rights.authorized(user, path, "Ww")
  50. parent_permissions = self.Rights.authorized(user, parent_path, "w")
  51. def prepare(vobject_items, tag=None, write_whole_collection=None):
  52. if (write_whole_collection or
  53. permissions and not parent_permissions):
  54. write_whole_collection = True
  55. content_type = environ.get("CONTENT_TYPE",
  56. "").split(";")[0]
  57. tags = {value: key
  58. for key, value in xmlutils.MIMETYPES.items()}
  59. tag = radicale_item.predict_tag_of_whole_collection(
  60. vobject_items, tags.get(content_type))
  61. if not tag:
  62. raise ValueError("Can't determine collection tag")
  63. collection_path = pathutils.strip_path(path)
  64. elif (write_whole_collection is not None and
  65. not write_whole_collection or
  66. not permissions and parent_permissions):
  67. write_whole_collection = False
  68. if tag is None:
  69. tag = storage.predict_tag_of_parent_collection(
  70. vobject_items)
  71. collection_path = posixpath.dirname(
  72. pathutils.strip_path(path))
  73. props = None
  74. stored_exc_info = None
  75. items = []
  76. try:
  77. if tag:
  78. radicale_item.check_and_sanitize_items(
  79. vobject_items, is_collection=write_whole_collection,
  80. tag=tag)
  81. if write_whole_collection and tag == "VCALENDAR":
  82. vobject_components = []
  83. vobject_item, = vobject_items
  84. for content in ("vevent", "vtodo", "vjournal"):
  85. vobject_components.extend(
  86. getattr(vobject_item, "%s_list" % content, []))
  87. vobject_components_by_uid = itertools.groupby(
  88. sorted(vobject_components,
  89. key=radicale_item.get_uid),
  90. radicale_item.get_uid)
  91. for uid, components in vobject_components_by_uid:
  92. vobject_collection = vobject.iCalendar()
  93. for component in components:
  94. vobject_collection.add(component)
  95. item = radicale_item.Item(
  96. collection_path=collection_path,
  97. vobject_item=vobject_collection)
  98. item.prepare()
  99. items.append(item)
  100. elif write_whole_collection and tag == "VADDRESSBOOK":
  101. for vobject_item in vobject_items:
  102. item = radicale_item.Item(
  103. collection_path=collection_path,
  104. vobject_item=vobject_item)
  105. item.prepare()
  106. items.append(item)
  107. elif not write_whole_collection:
  108. vobject_item, = vobject_items
  109. item = radicale_item.Item(
  110. collection_path=collection_path,
  111. vobject_item=vobject_item)
  112. item.prepare()
  113. items.append(item)
  114. if write_whole_collection:
  115. props = {}
  116. if tag:
  117. props["tag"] = tag
  118. if tag == "VCALENDAR" and vobject_items:
  119. if hasattr(vobject_items[0], "x_wr_calname"):
  120. calname = vobject_items[0].x_wr_calname.value
  121. if calname:
  122. props["D:displayname"] = calname
  123. if hasattr(vobject_items[0], "x_wr_caldesc"):
  124. caldesc = vobject_items[0].x_wr_caldesc.value
  125. if caldesc:
  126. props["C:calendar-description"] = caldesc
  127. radicale_item.check_and_sanitize_props(props)
  128. except Exception:
  129. stored_exc_info = sys.exc_info()
  130. # Use generator for items and delete references to free memory
  131. # early
  132. def items_generator():
  133. while items:
  134. yield items.pop(0)
  135. return (items_generator(), tag, write_whole_collection, props,
  136. stored_exc_info)
  137. try:
  138. vobject_items = tuple(vobject.readComponents(content or ""))
  139. except Exception as e:
  140. logger.warning(
  141. "Bad PUT request on %r: %s", path, e, exc_info=True)
  142. return httputils.BAD_REQUEST
  143. (prepared_items, prepared_tag, prepared_write_whole_collection,
  144. prepared_props, prepared_exc_info) = prepare(vobject_items)
  145. with self.Collection.acquire_lock("w", user):
  146. item = next(self.Collection.discover(path), None)
  147. parent_item = next(self.Collection.discover(parent_path), None)
  148. if not parent_item:
  149. return httputils.CONFLICT
  150. write_whole_collection = (
  151. isinstance(item, storage.BaseCollection) or
  152. not parent_item.get_meta("tag"))
  153. if write_whole_collection:
  154. tag = prepared_tag
  155. else:
  156. tag = parent_item.get_meta("tag")
  157. if write_whole_collection:
  158. if not self.Rights.authorized(user, path, "w" if tag else "W"):
  159. return httputils.NOT_ALLOWED
  160. elif not self.Rights.authorized(user, parent_path, "w"):
  161. return httputils.NOT_ALLOWED
  162. etag = environ.get("HTTP_IF_MATCH", "")
  163. if not item and etag:
  164. # Etag asked but no item found: item has been removed
  165. return httputils.PRECONDITION_FAILED
  166. if item and etag and item.etag != etag:
  167. # Etag asked but item not matching: item has changed
  168. return httputils.PRECONDITION_FAILED
  169. match = environ.get("HTTP_IF_NONE_MATCH", "") == "*"
  170. if item and match:
  171. # Creation asked but item found: item can't be replaced
  172. return httputils.PRECONDITION_FAILED
  173. if (tag != prepared_tag or
  174. prepared_write_whole_collection != write_whole_collection):
  175. (prepared_items, prepared_tag, prepared_write_whole_collection,
  176. prepared_props, prepared_exc_info) = prepare(
  177. vobject_items, tag, write_whole_collection)
  178. props = prepared_props
  179. if prepared_exc_info:
  180. logger.warning(
  181. "Bad PUT request on %r: %s", path, prepared_exc_info[1],
  182. exc_info=prepared_exc_info)
  183. return httputils.BAD_REQUEST
  184. if write_whole_collection:
  185. try:
  186. etag = self.Collection.create_collection(
  187. path, prepared_items, props).etag
  188. except ValueError as e:
  189. logger.warning(
  190. "Bad PUT request on %r: %s", path, e, exc_info=True)
  191. return httputils.BAD_REQUEST
  192. else:
  193. prepared_item, = prepared_items
  194. if (item and item.uid != prepared_item.uid or
  195. not item and parent_item.has_uid(prepared_item.uid)):
  196. return self.webdav_error_response(
  197. "C" if tag == "VCALENDAR" else "CR",
  198. "no-uid-conflict")
  199. href = posixpath.basename(pathutils.strip_path(path))
  200. try:
  201. etag = parent_item.upload(href, prepared_item).etag
  202. except ValueError as e:
  203. logger.warning(
  204. "Bad PUT request on %r: %s", path, e, exc_info=True)
  205. return httputils.BAD_REQUEST
  206. headers = {"ETag": etag}
  207. return client.CREATED, headers, None