xmlutils.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. # This file is part of Radicale Server - Calendar Server
  2. # Copyright © 2008 Nicolas Kandel
  3. # Copyright © 2008 Pascal Halter
  4. # Copyright © 2008-2015 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. XML and iCal requests manager.
  21. Note that all these functions need to receive unicode objects for full
  22. iCal requests (PUT) and string objects with charset correctly defined
  23. in them for XML requests (all but PUT).
  24. """
  25. import copy
  26. import re
  27. import xml.etree.ElementTree as ET
  28. from collections import OrderedDict
  29. from http import client
  30. from urllib.parse import quote
  31. from radicale import pathutils
  32. MIMETYPES = {
  33. "VADDRESSBOOK": "text/vcard",
  34. "VCALENDAR": "text/calendar"}
  35. OBJECT_MIMETYPES = {
  36. "VCARD": "text/vcard",
  37. "VLIST": "text/x-vlist",
  38. "VCALENDAR": "text/calendar"}
  39. NAMESPACES = {
  40. "C": "urn:ietf:params:xml:ns:caldav",
  41. "CR": "urn:ietf:params:xml:ns:carddav",
  42. "D": "DAV:",
  43. "CS": "http://calendarserver.org/ns/",
  44. "ICAL": "http://apple.com/ns/ical/",
  45. "ME": "http://me.com/_namespace/",
  46. "RADICALE": "http://radicale.org/ns/"}
  47. NAMESPACES_REV = {}
  48. for short, url in NAMESPACES.items():
  49. NAMESPACES_REV[url] = short
  50. ET.register_namespace("" if short == "D" else short, url)
  51. CLARK_TAG_REGEX = re.compile(r"{(?P<namespace>[^}]*)}(?P<tag>.*)", re.VERBOSE)
  52. HUMAN_REGEX = re.compile(r"(?P<namespace>[^:{}]*):(?P<tag>.*)", re.VERBOSE)
  53. def pretty_xml(element, level=0):
  54. """Indent an ElementTree ``element`` and its children."""
  55. if not level:
  56. element = copy.deepcopy(element)
  57. i = "\n" + level * " "
  58. if len(element):
  59. if not element.text or not element.text.strip():
  60. element.text = i + " "
  61. if not element.tail or not element.tail.strip():
  62. element.tail = i
  63. for sub_element in element:
  64. pretty_xml(sub_element, level + 1)
  65. if not sub_element.tail or not sub_element.tail.strip():
  66. sub_element.tail = i
  67. else:
  68. if level and (not element.tail or not element.tail.strip()):
  69. element.tail = i
  70. if not level:
  71. return '<?xml version="1.0"?>\n%s' % ET.tostring(element, "unicode")
  72. def make_tag(short_name, local):
  73. """Get XML Clark notation {uri(``short_name``)}``local``."""
  74. return "{%s}%s" % (NAMESPACES[short_name], local)
  75. def tag_from_clark(name):
  76. """Get a human-readable variant of the XML Clark notation tag ``name``.
  77. For a given name using the XML Clark notation, return a human-readable
  78. variant of the tag name for known namespaces. Otherwise, return the name as
  79. is.
  80. """
  81. match = CLARK_TAG_REGEX.match(name)
  82. if match and match.group("namespace") in NAMESPACES_REV:
  83. args = {
  84. "ns": NAMESPACES_REV[match.group("namespace")],
  85. "tag": match.group("tag")}
  86. return "%(ns)s:%(tag)s" % args
  87. return name
  88. def tag_from_human(name):
  89. """Get an XML Clark notation tag from human-readable variant ``name``."""
  90. match = HUMAN_REGEX.match(name)
  91. if match and match.group("namespace") in NAMESPACES:
  92. return make_tag(match.group("namespace"), match.group("tag"))
  93. return name
  94. def make_response(code):
  95. """Return full W3C names from HTTP status codes."""
  96. return "HTTP/1.1 %i %s" % (code, client.responses[code])
  97. def make_href(base_prefix, href):
  98. """Return prefixed href."""
  99. assert href == pathutils.sanitize_path(href)
  100. return quote("%s%s" % (base_prefix, href))
  101. def webdav_error(namespace, name):
  102. """Generate XML error message."""
  103. root = ET.Element(make_tag("D", "error"))
  104. root.append(ET.Element(make_tag(namespace, name)))
  105. return root
  106. def get_content_type(item):
  107. """Get the content-type of an item with charset and component parameters.
  108. """
  109. mimetype = OBJECT_MIMETYPES[item.name]
  110. encoding = item.collection.configuration.get("encoding", "request")
  111. tag = item.component_name
  112. content_type = "%s;charset=%s" % (mimetype, encoding)
  113. if tag:
  114. content_type += ";component=%s" % tag
  115. return content_type
  116. def props_from_request(xml_request, actions=("set", "remove")):
  117. """Return a list of properties as a dictionary."""
  118. result = OrderedDict()
  119. if xml_request is None:
  120. return result
  121. for action in actions:
  122. action_element = xml_request.find(make_tag("D", action))
  123. if action_element is not None:
  124. break
  125. else:
  126. action_element = xml_request
  127. prop_element = action_element.find(make_tag("D", "prop"))
  128. if prop_element is not None:
  129. for prop in prop_element:
  130. if prop.tag == make_tag("D", "resourcetype"):
  131. for resource_type in prop:
  132. if resource_type.tag == make_tag("C", "calendar"):
  133. result["tag"] = "VCALENDAR"
  134. break
  135. elif resource_type.tag == make_tag("CR", "addressbook"):
  136. result["tag"] = "VADDRESSBOOK"
  137. break
  138. elif prop.tag == make_tag("C", "supported-calendar-component-set"):
  139. result[tag_from_clark(prop.tag)] = ",".join(
  140. supported_comp.attrib["name"]
  141. for supported_comp in prop
  142. if supported_comp.tag == make_tag("C", "comp"))
  143. else:
  144. result[tag_from_clark(prop.tag)] = prop.text
  145. return result