xmlutils.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  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. Helper functions for XML.
  21. """
  22. import copy
  23. import xml.etree.ElementTree as ET
  24. from collections import OrderedDict
  25. from http import client
  26. from urllib.parse import quote
  27. from radicale import pathutils
  28. MIMETYPES = {
  29. "VADDRESSBOOK": "text/vcard",
  30. "VCALENDAR": "text/calendar"}
  31. OBJECT_MIMETYPES = {
  32. "VCARD": "text/vcard",
  33. "VLIST": "text/x-vlist",
  34. "VCALENDAR": "text/calendar"}
  35. NAMESPACES = {
  36. "C": "urn:ietf:params:xml:ns:caldav",
  37. "CR": "urn:ietf:params:xml:ns:carddav",
  38. "D": "DAV:",
  39. "CS": "http://calendarserver.org/ns/",
  40. "ICAL": "http://apple.com/ns/ical/",
  41. "ME": "http://me.com/_namespace/",
  42. "RADICALE": "http://radicale.org/ns/"}
  43. NAMESPACES_REV = {}
  44. for short, url in NAMESPACES.items():
  45. NAMESPACES_REV[url] = short
  46. ET.register_namespace("" if short == "D" else short, url)
  47. def pretty_xml(element):
  48. """Indent an ElementTree ``element`` and its children."""
  49. def pretty_xml_recursive(element, level):
  50. indent = "\n" + level * " "
  51. if len(element) > 0:
  52. if not (element.text or "").strip():
  53. element.text = indent + " "
  54. if not (element.tail or "").strip():
  55. element.tail = indent
  56. for sub_element in element:
  57. pretty_xml_recursive(sub_element, level + 1)
  58. if not (sub_element.tail or "").strip():
  59. sub_element.tail = indent
  60. elif level > 0 and not (element.tail or "").strip():
  61. element.tail = indent
  62. element = copy.deepcopy(element)
  63. pretty_xml_recursive(element, 0)
  64. return '<?xml version="1.0"?>\n%s' % ET.tostring(element, "unicode")
  65. def make_clark(human_tag):
  66. """Get XML Clark notation from human tag ``human_tag``.
  67. If ``human_tag`` is already in XML Clark notation it is returned as-is.
  68. """
  69. if human_tag.startswith("{"):
  70. ns, tag = human_tag[len("{"):].split("}", maxsplit=1)
  71. if not ns or not tag:
  72. raise ValueError("Invalid XML tag: %r" % human_tag)
  73. return human_tag
  74. ns_prefix, tag = human_tag.split(":", maxsplit=1)
  75. if not ns_prefix or not tag:
  76. raise ValueError("Invalid XML tag: %r" % human_tag)
  77. ns = NAMESPACES.get(ns_prefix)
  78. if not ns:
  79. raise ValueError("Unknown XML namespace prefix: %r" % human_tag)
  80. return "{%s}%s" % (ns, tag)
  81. def make_human_tag(clark_tag):
  82. """Replace known namespaces in XML Clark notation ``clark_tag`` with
  83. prefix.
  84. If the namespace is not in ``NAMESPACES`` the tag is returned as-is.
  85. """
  86. if not clark_tag.startswith("{"):
  87. ns_prefix, tag = clark_tag.split(":", maxsplit=1)
  88. if not ns_prefix or not tag:
  89. raise ValueError("Invalid XML tag: %r" % clark_tag)
  90. if ns_prefix not in NAMESPACES:
  91. raise ValueError("Unknown XML namespace prefix: %r" % clark_tag)
  92. return clark_tag
  93. ns, tag = clark_tag[len("{"):].split("}", maxsplit=1)
  94. if not ns or not tag:
  95. raise ValueError("Invalid XML tag: %r" % clark_tag)
  96. ns_prefix = NAMESPACES_REV.get(ns)
  97. if ns_prefix:
  98. return "%s:%s" % (ns_prefix, tag)
  99. return clark_tag
  100. def make_response(code):
  101. """Return full W3C names from HTTP status codes."""
  102. return "HTTP/1.1 %i %s" % (code, client.responses[code])
  103. def make_href(base_prefix, href):
  104. """Return prefixed href."""
  105. assert href == pathutils.sanitize_path(href)
  106. return quote("%s%s" % (base_prefix, href))
  107. def webdav_error(human_tag):
  108. """Generate XML error message."""
  109. root = ET.Element(make_clark("D:error"))
  110. root.append(ET.Element(make_clark(human_tag)))
  111. return root
  112. def get_content_type(item, encoding):
  113. """Get the content-type of an item with charset and component parameters.
  114. """
  115. mimetype = OBJECT_MIMETYPES[item.name]
  116. tag = item.component_name
  117. content_type = "%s;charset=%s" % (mimetype, encoding)
  118. if tag:
  119. content_type += ";component=%s" % tag
  120. return content_type
  121. def props_from_request(xml_request, actions=("set", "remove")):
  122. """Return a list of properties as a dictionary."""
  123. result = OrderedDict()
  124. if xml_request is None:
  125. return result
  126. for action in actions:
  127. action_element = xml_request.find(make_clark("D:%s" % action))
  128. if action_element is not None:
  129. break
  130. else:
  131. action_element = xml_request
  132. prop_element = action_element.find(make_clark("D:prop"))
  133. if prop_element is not None:
  134. for prop in prop_element:
  135. if prop.tag == make_clark("D:resourcetype"):
  136. for resource_type in prop:
  137. if resource_type.tag == make_clark("C:calendar"):
  138. result["tag"] = "VCALENDAR"
  139. break
  140. if resource_type.tag == make_clark("CR:addressbook"):
  141. result["tag"] = "VADDRESSBOOK"
  142. break
  143. elif prop.tag == make_clark("C:supported-calendar-component-set"):
  144. result[make_human_tag(prop.tag)] = ",".join(
  145. supported_comp.attrib["name"]
  146. for supported_comp in prop
  147. if supported_comp.tag == make_clark("C:comp"))
  148. else:
  149. result[make_human_tag(prop.tag)] = prop.text
  150. return result