xmlutils.py 5.7 KB

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