1
0

propfind.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  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 collections
  20. import itertools
  21. import posixpath
  22. import socket
  23. from http import client
  24. from xml.etree import ElementTree as ET
  25. from radicale import app, httputils, pathutils, rights, storage, xmlutils
  26. from radicale.log import logger
  27. def xml_propfind(base_prefix, path, xml_request, allowed_items, user,
  28. encoding):
  29. """Read and answer PROPFIND requests.
  30. Read rfc4918-9.1 for info.
  31. The collections parameter is a list of collections that are to be included
  32. in the output.
  33. """
  34. # A client may choose not to submit a request body. An empty PROPFIND
  35. # request body MUST be treated as if it were an 'allprop' request.
  36. top_element = (xml_request[0] if xml_request is not None else
  37. ET.Element(xmlutils.make_clark("D:allprop")))
  38. props = ()
  39. allprop = False
  40. propname = False
  41. if top_element.tag == xmlutils.make_clark("D:allprop"):
  42. allprop = True
  43. elif top_element.tag == xmlutils.make_clark("D:propname"):
  44. propname = True
  45. elif top_element.tag == xmlutils.make_clark("D:prop"):
  46. props = [prop.tag for prop in top_element]
  47. if xmlutils.make_clark("D:current-user-principal") in props and not user:
  48. # Ask for authentication
  49. # Returning the DAV:unauthenticated pseudo-principal as specified in
  50. # RFC 5397 doesn't seem to work with DAVx5.
  51. return client.FORBIDDEN, None
  52. # Writing answer
  53. multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
  54. for item, permission in allowed_items:
  55. write = permission == "w"
  56. multistatus.append(xml_propfind_response(
  57. base_prefix, path, item, props, user, encoding, write=write,
  58. allprop=allprop, propname=propname))
  59. return client.MULTI_STATUS, multistatus
  60. def xml_propfind_response(base_prefix, path, item, props, user, encoding,
  61. write=False, propname=False, allprop=False):
  62. """Build and return a PROPFIND response."""
  63. if propname and allprop or (props and (propname or allprop)):
  64. raise ValueError("Only use one of props, propname and allprops")
  65. is_collection = isinstance(item, storage.BaseCollection)
  66. if is_collection:
  67. is_leaf = item.get_meta("tag") in ("VADDRESSBOOK", "VCALENDAR")
  68. collection = item
  69. else:
  70. collection = item.collection
  71. response = ET.Element(xmlutils.make_clark("D:response"))
  72. href = ET.Element(xmlutils.make_clark("D:href"))
  73. if is_collection:
  74. # Some clients expect collections to end with /
  75. uri = pathutils.unstrip_path(item.path, True)
  76. else:
  77. uri = pathutils.unstrip_path(
  78. posixpath.join(collection.path, item.href))
  79. href.text = xmlutils.make_href(base_prefix, uri)
  80. response.append(href)
  81. if propname or allprop:
  82. props = []
  83. # Should list all properties that can be retrieved by the code below
  84. props.append(xmlutils.make_clark("D:principal-collection-set"))
  85. props.append(xmlutils.make_clark("D:current-user-principal"))
  86. props.append(xmlutils.make_clark("D:current-user-privilege-set"))
  87. props.append(xmlutils.make_clark("D:supported-report-set"))
  88. props.append(xmlutils.make_clark("D:resourcetype"))
  89. props.append(xmlutils.make_clark("D:owner"))
  90. if is_collection and collection.is_principal:
  91. props.append(xmlutils.make_clark("C:calendar-user-address-set"))
  92. props.append(xmlutils.make_clark("D:principal-URL"))
  93. props.append(xmlutils.make_clark("CR:addressbook-home-set"))
  94. props.append(xmlutils.make_clark("C:calendar-home-set"))
  95. if not is_collection or is_leaf:
  96. props.append(xmlutils.make_clark("D:getetag"))
  97. props.append(xmlutils.make_clark("D:getlastmodified"))
  98. props.append(xmlutils.make_clark("D:getcontenttype"))
  99. props.append(xmlutils.make_clark("D:getcontentlength"))
  100. if is_collection:
  101. if is_leaf:
  102. props.append(xmlutils.make_clark("D:displayname"))
  103. props.append(xmlutils.make_clark("D:sync-token"))
  104. if collection.get_meta("tag") == "VCALENDAR":
  105. props.append(xmlutils.make_clark("CS:getctag"))
  106. props.append(
  107. xmlutils.make_clark("C:supported-calendar-component-set"))
  108. meta = item.get_meta()
  109. for tag in meta:
  110. if tag == "tag":
  111. continue
  112. clark_tag = xmlutils.make_clark(tag)
  113. if clark_tag not in props:
  114. props.append(clark_tag)
  115. responses = collections.defaultdict(list)
  116. if propname:
  117. for tag in props:
  118. responses[200].append(ET.Element(tag))
  119. props = ()
  120. for tag in props:
  121. element = ET.Element(tag)
  122. is404 = False
  123. if tag == xmlutils.make_clark("D:getetag"):
  124. if not is_collection or is_leaf:
  125. element.text = item.etag
  126. else:
  127. is404 = True
  128. elif tag == xmlutils.make_clark("D:getlastmodified"):
  129. if not is_collection or is_leaf:
  130. element.text = item.last_modified
  131. else:
  132. is404 = True
  133. elif tag == xmlutils.make_clark("D:principal-collection-set"):
  134. child_element = ET.Element(xmlutils.make_clark("D:href"))
  135. child_element.text = xmlutils.make_href(base_prefix, "/")
  136. element.append(child_element)
  137. elif (tag in (xmlutils.make_clark("C:calendar-user-address-set"),
  138. xmlutils.make_clark("D:principal-URL"),
  139. xmlutils.make_clark("CR:addressbook-home-set"),
  140. xmlutils.make_clark("C:calendar-home-set")) and
  141. collection.is_principal and is_collection):
  142. child_element = ET.Element(xmlutils.make_clark("D:href"))
  143. child_element.text = xmlutils.make_href(base_prefix, path)
  144. element.append(child_element)
  145. elif tag == xmlutils.make_clark("C:supported-calendar-component-set"):
  146. human_tag = xmlutils.make_human_tag(tag)
  147. if is_collection and is_leaf:
  148. meta = item.get_meta(human_tag)
  149. if meta:
  150. components = meta.split(",")
  151. else:
  152. components = ("VTODO", "VEVENT", "VJOURNAL")
  153. for component in components:
  154. comp = ET.Element(xmlutils.make_clark("C:comp"))
  155. comp.set("name", component)
  156. element.append(comp)
  157. else:
  158. is404 = True
  159. elif tag == xmlutils.make_clark("D:current-user-principal"):
  160. if user:
  161. child_element = ET.Element(xmlutils.make_clark("D:href"))
  162. child_element.text = xmlutils.make_href(
  163. base_prefix, "/%s/" % user)
  164. element.append(child_element)
  165. else:
  166. element.append(ET.Element(
  167. xmlutils.make_clark("D:unauthenticated")))
  168. elif tag == xmlutils.make_clark("D:current-user-privilege-set"):
  169. privileges = ["D:read"]
  170. if write:
  171. privileges.append("D:all")
  172. privileges.append("D:write")
  173. privileges.append("D:write-properties")
  174. privileges.append("D:write-content")
  175. for human_tag in privileges:
  176. privilege = ET.Element(xmlutils.make_clark("D:privilege"))
  177. privilege.append(ET.Element(
  178. xmlutils.make_clark(human_tag)))
  179. element.append(privilege)
  180. elif tag == xmlutils.make_clark("D:supported-report-set"):
  181. # These 3 reports are not implemented
  182. reports = ["D:expand-property",
  183. "D:principal-search-property-set",
  184. "D:principal-property-search"]
  185. if is_collection and is_leaf:
  186. reports.append("D:sync-collection")
  187. if item.get_meta("tag") == "VADDRESSBOOK":
  188. reports.append("CR:addressbook-multiget")
  189. reports.append("CR:addressbook-query")
  190. elif item.get_meta("tag") == "VCALENDAR":
  191. reports.append("C:calendar-multiget")
  192. reports.append("C:calendar-query")
  193. for human_tag in reports:
  194. supported_report = ET.Element(
  195. xmlutils.make_clark("D:supported-report"))
  196. report_element = ET.Element(xmlutils.make_clark("D:report"))
  197. report_element.append(
  198. ET.Element(xmlutils.make_clark(human_tag)))
  199. supported_report.append(report_element)
  200. element.append(supported_report)
  201. elif tag == xmlutils.make_clark("D:getcontentlength"):
  202. if not is_collection or is_leaf:
  203. element.text = str(len(item.serialize().encode(encoding)))
  204. else:
  205. is404 = True
  206. elif tag == xmlutils.make_clark("D:owner"):
  207. # return empty elment, if no owner available (rfc3744-5.1)
  208. if collection.owner:
  209. child_element = ET.Element(xmlutils.make_clark("D:href"))
  210. child_element.text = xmlutils.make_href(
  211. base_prefix, "/%s/" % collection.owner)
  212. element.append(child_element)
  213. elif is_collection:
  214. if tag == xmlutils.make_clark("D:getcontenttype"):
  215. if is_leaf:
  216. element.text = xmlutils.MIMETYPES[item.get_meta("tag")]
  217. else:
  218. is404 = True
  219. elif tag == xmlutils.make_clark("D:resourcetype"):
  220. if item.is_principal:
  221. child_element = ET.Element(
  222. xmlutils.make_clark("D:principal"))
  223. element.append(child_element)
  224. if is_leaf:
  225. if item.get_meta("tag") == "VADDRESSBOOK":
  226. child_element = ET.Element(
  227. xmlutils.make_clark("CR:addressbook"))
  228. element.append(child_element)
  229. elif item.get_meta("tag") == "VCALENDAR":
  230. child_element = ET.Element(
  231. xmlutils.make_clark("C:calendar"))
  232. element.append(child_element)
  233. child_element = ET.Element(xmlutils.make_clark("D:collection"))
  234. element.append(child_element)
  235. elif tag == xmlutils.make_clark("RADICALE:displayname"):
  236. # Only for internal use by the web interface
  237. displayname = item.get_meta("D:displayname")
  238. if displayname is not None:
  239. element.text = displayname
  240. else:
  241. is404 = True
  242. elif tag == xmlutils.make_clark("D:displayname"):
  243. displayname = item.get_meta("D:displayname")
  244. if not displayname and is_leaf:
  245. displayname = item.path
  246. if displayname is not None:
  247. element.text = displayname
  248. else:
  249. is404 = True
  250. elif tag == xmlutils.make_clark("CS:getctag"):
  251. if is_leaf:
  252. element.text = item.etag
  253. else:
  254. is404 = True
  255. elif tag == xmlutils.make_clark("D:sync-token"):
  256. if is_leaf:
  257. element.text, _ = item.sync()
  258. else:
  259. is404 = True
  260. else:
  261. human_tag = xmlutils.make_human_tag(tag)
  262. meta = item.get_meta(human_tag)
  263. if meta is not None:
  264. element.text = meta
  265. else:
  266. is404 = True
  267. # Not for collections
  268. elif tag == xmlutils.make_clark("D:getcontenttype"):
  269. element.text = xmlutils.get_content_type(item, encoding)
  270. elif tag == xmlutils.make_clark("D:resourcetype"):
  271. # resourcetype must be returned empty for non-collection elements
  272. pass
  273. else:
  274. is404 = True
  275. responses[404 if is404 else 200].append(element)
  276. for status_code, childs in responses.items():
  277. if not childs:
  278. continue
  279. propstat = ET.Element(xmlutils.make_clark("D:propstat"))
  280. response.append(propstat)
  281. prop = ET.Element(xmlutils.make_clark("D:prop"))
  282. prop.extend(childs)
  283. propstat.append(prop)
  284. status = ET.Element(xmlutils.make_clark("D:status"))
  285. status.text = xmlutils.make_response(status_code)
  286. propstat.append(status)
  287. return response
  288. class ApplicationPropfindMixin:
  289. def _collect_allowed_items(self, items, user):
  290. """Get items from request that user is allowed to access."""
  291. for item in items:
  292. if isinstance(item, storage.BaseCollection):
  293. path = pathutils.unstrip_path(item.path, True)
  294. if item.get_meta("tag"):
  295. permissions = rights.intersect(
  296. self._rights.authorization(user, path), "rw")
  297. target = "collection with tag %r" % item.path
  298. else:
  299. permissions = rights.intersect(
  300. self._rights.authorization(user, path), "RW")
  301. target = "collection %r" % item.path
  302. else:
  303. path = pathutils.unstrip_path(item.collection.path, True)
  304. permissions = rights.intersect(
  305. self._rights.authorization(user, path), "rw")
  306. target = "item %r from %r" % (item.href, item.collection.path)
  307. if rights.intersect(permissions, "Ww"):
  308. permission = "w"
  309. status = "write"
  310. elif rights.intersect(permissions, "Rr"):
  311. permission = "r"
  312. status = "read"
  313. else:
  314. permission = ""
  315. status = "NO"
  316. logger.debug(
  317. "%s has %s access to %s",
  318. repr(user) if user else "anonymous user", status, target)
  319. if permission:
  320. yield item, permission
  321. def do_PROPFIND(self, environ, base_prefix, path, user):
  322. """Manage PROPFIND request."""
  323. access = app.Access(self._rights, user, path)
  324. if not access.check("r"):
  325. return httputils.NOT_ALLOWED
  326. try:
  327. xml_content = self._read_xml_request_body(environ)
  328. except RuntimeError as e:
  329. logger.warning(
  330. "Bad PROPFIND request on %r: %s", path, e, exc_info=True)
  331. return httputils.BAD_REQUEST
  332. except socket.timeout:
  333. logger.debug("client timed out", exc_info=True)
  334. return httputils.REQUEST_TIMEOUT
  335. with self._storage.acquire_lock("r", user):
  336. items = self._storage.discover(
  337. path, environ.get("HTTP_DEPTH", "0"))
  338. # take root item for rights checking
  339. item = next(items, None)
  340. if not item:
  341. return httputils.NOT_FOUND
  342. if not access.check("r", item):
  343. return httputils.NOT_ALLOWED
  344. # put item back
  345. items = itertools.chain([item], items)
  346. allowed_items = self._collect_allowed_items(items, user)
  347. headers = {"DAV": httputils.DAV_HEADERS,
  348. "Content-Type": "text/xml; charset=%s" % self._encoding}
  349. status, xml_answer = xml_propfind(
  350. base_prefix, path, xml_content, allowed_items, user,
  351. self._encoding)
  352. if status == client.FORBIDDEN and xml_answer is None:
  353. return httputils.NOT_ALLOWED
  354. return status, headers, self._xml_response(xml_answer)