put.py 9.9 KB

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