put.py 10.0 KB

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