Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

291 řádky
11 KiB

  1. #!/usr/bin/env python
  2. # vim: set ts=4 sw=4 expandtab sts=4:
  3. # Copyright (c) 2011-2013 Christian Geier & contributors
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining
  6. # a copy of this software and associated documentation files (the
  7. # "Software"), to deal in the Software without restriction, including
  8. # without limitation the rights to use, copy, modify, merge, publish,
  9. # distribute, sublicense, and/or sell copies of the Software, and to
  10. # permit persons to whom the Software is furnished to do so, subject to
  11. # the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be
  14. # included in all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  18. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  20. # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  21. # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  22. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. #-------------------------------------------------------------------------------
  24. # Lukasz Janyst:
  25. #
  26. # lxml encoding issue:
  27. # * Remove '<?xml version="1.0" encoding="utf-8"?>' header from responses
  28. # to prevent etree errors
  29. #
  30. # requests-0.8.2:
  31. # * Remove the verify ssl flag - caused exception
  32. # * Add own raise_for_status for more meaningful error messages
  33. # * Fix digest auth
  34. #-------------------------------------------------------------------------------
  35. """
  36. contains the class PyCardDAV and some associated functions and definitions
  37. """
  38. from collections import namedtuple
  39. import requests
  40. import sys
  41. import urllib.parse as urlparse
  42. import logging
  43. import lxml.etree as ET
  44. import string
  45. def raise_for_status( resp ):
  46. if 400 <= resp.status_code < 500 or 500 <= resp.status_code < 600:
  47. msg = 'Error code: ' + str(resp.status_code) + '\n'
  48. msg += resp.content
  49. raise requests.exceptions.HTTPError( msg )
  50. def get_random_href():
  51. """returns a random href"""
  52. import random
  53. tmp_list = list()
  54. for _ in xrange(3):
  55. rand_number = random.randint(0, 0x100000000)
  56. tmp_list.append("{0:x}".format(rand_number))
  57. return "-".join(tmp_list).upper()
  58. DAVICAL = 'davical'
  59. SABREDAV = 'sabredav'
  60. UNKNOWN = 'unknown server'
  61. class UploadFailed(Exception):
  62. """uploading the card failed"""
  63. pass
  64. class PyCardDAV(object):
  65. """class for interacting with a CardDAV server
  66. Since PyCardDAV relies heavily on Requests [1] its SSL verification is also
  67. shared by PyCardDAV [2]. For now, only the *verify* keyword is exposed
  68. through PyCardDAV.
  69. [1] http://docs.python-requests.org/
  70. [2] http://docs.python-requests.org/en/latest/user/advanced/
  71. raises:
  72. requests.exceptions.SSLError
  73. requests.exceptions.ConnectionError
  74. more requests.exceptions depending on the actual error
  75. Exception (shame on me)
  76. """
  77. def __init__(self, resource, debug='', user='', passwd='',
  78. verify=True, write_support=False, auth='basic'):
  79. #shutup url3
  80. urllog = logging.getLogger('requests.packages.urllib3.connectionpool')
  81. urllog.setLevel(logging.CRITICAL)
  82. split_url = urlparse.urlparse(resource)
  83. url_tuple = namedtuple('url', 'resource base path')
  84. self.url = url_tuple(resource,
  85. split_url.scheme + '://' + split_url.netloc,
  86. split_url.path)
  87. self.debug = debug
  88. self.session = requests.session()
  89. self.write_support = write_support
  90. self._settings = {'verify': verify}
  91. if auth == 'basic':
  92. self._settings['auth'] = (user, passwd,)
  93. if auth == 'digest':
  94. from requests.auth import HTTPDigestAuth
  95. self._settings['auth'] = HTTPDigestAuth(user, passwd)
  96. self._default_headers = {"User-Agent": "pyCardDAV"}
  97. response = self.session.request('PROPFIND', resource,
  98. headers=self.headers,
  99. **self._settings)
  100. raise_for_status( response ) #raises error on not 2XX HTTP status code
  101. @property
  102. def verify(self):
  103. """gets verify from settings dict"""
  104. return self._settings['verify']
  105. @verify.setter
  106. def verify(self, verify):
  107. """set verify"""
  108. self._settings['verify'] = verify
  109. @property
  110. def headers(self):
  111. return dict(self._default_headers)
  112. def _check_write_support(self):
  113. """checks if user really wants his data destroyed"""
  114. if not self.write_support:
  115. sys.stderr.write("Sorry, no write support for you. Please check "
  116. "the documentation.\n")
  117. sys.exit(1)
  118. def _detect_server(self):
  119. """detects CardDAV server type
  120. currently supports davical and sabredav (same as owncloud)
  121. :rtype: string "davical" or "sabredav"
  122. """
  123. response = requests.request('OPTIONS',
  124. self.url.base,
  125. headers=self.header)
  126. if "X-Sabre-Version" in response.headers:
  127. server = SABREDAV
  128. elif "X-DAViCal-Version" in response.headers:
  129. server = DAVICAL
  130. else:
  131. server = UNKNOWN
  132. logging.info(server + " detected")
  133. return server
  134. def get_abook(self):
  135. """does the propfind and processes what it returns
  136. :rtype: list of hrefs to vcards
  137. """
  138. xml = self._get_xml_props()
  139. abook = self._process_xml_props(xml)
  140. return abook
  141. def get_vcard(self, href):
  142. """
  143. pulls vcard from server
  144. :returns: vcard
  145. :rtype: string
  146. """
  147. response = self.session.get(self.url.base + href,
  148. headers=self.headers,
  149. **self._settings)
  150. raise_for_status( response )
  151. return response.content
  152. def update_vcard(self, card, href, etag):
  153. """
  154. pushes changed vcard to the server
  155. card: vcard as unicode string
  156. etag: str or None, if this is set to a string, card is only updated if
  157. remote etag matches. If etag = None the update is forced anyway
  158. """
  159. # TODO what happens if etag does not match?
  160. self._check_write_support()
  161. remotepath = str(self.url.base + href)
  162. headers = self.headers
  163. headers['content-type'] = 'text/vcard'
  164. if etag is not None:
  165. headers['If-Match'] = etag
  166. self.session.put(remotepath, data=card.encode('utf-8'), headers=headers,
  167. **self._settings)
  168. def delete_vcard(self, href, etag):
  169. """deletes vcard from server
  170. deletes the resource at href if etag matches,
  171. if etag=None delete anyway
  172. :param href: href of card to be deleted
  173. :type href: str()
  174. :param etag: etag of that card, if None card is always deleted
  175. :type href: str()
  176. :returns: nothing
  177. """
  178. # TODO: what happens if etag does not match, url does not exist etc ?
  179. self._check_write_support()
  180. remotepath = str(self.url.base + href)
  181. headers = self.headers
  182. headers['content-type'] = 'text/vcard'
  183. if etag is not None:
  184. headers['If-Match'] = etag
  185. result = self.session.delete(remotepath,
  186. headers=headers,
  187. **self._settings)
  188. raise_for_status( response )
  189. def upload_new_card(self, card):
  190. """
  191. upload new card to the server
  192. :param card: vcard to be uploaded
  193. :type card: unicode
  194. :rtype: tuple of string (path of the vcard on the server) and etag of
  195. new card (string or None)
  196. """
  197. self._check_write_support()
  198. card = card.encode('utf-8')
  199. for _ in range(0, 5):
  200. rand_string = get_random_href()
  201. remotepath = str(self.url.resource + rand_string + ".vcf")
  202. headers = self.headers
  203. headers['content-type'] = 'text/vcard'
  204. headers['If-None-Match'] = '*'
  205. response = requests.put(remotepath, data=card, headers=headers,
  206. **self._settings)
  207. if response.ok:
  208. parsed_url = urlparse.urlparse(remotepath)
  209. if 'etag' not in response.headers:
  210. etag = ''
  211. else:
  212. etag = response.headers['etag']
  213. return (parsed_url.path, etag)
  214. raise_for_status( response )
  215. def _get_xml_props(self):
  216. """PROPFIND method
  217. gets the xml file with all vcard hrefs
  218. :rtype: str() (an xml file)
  219. """
  220. headers = self.headers
  221. headers['Depth'] = '1'
  222. response = self.session.request('PROPFIND',
  223. self.url.resource,
  224. headers=headers,
  225. **self._settings)
  226. raise_for_status( response )
  227. if response.headers['DAV'].count('addressbook') == 0:
  228. raise Exception("URL is not a CardDAV resource")
  229. return response.content
  230. @classmethod
  231. def _process_xml_props(cls, xml):
  232. """processes the xml from PROPFIND, listing all vcard hrefs
  233. :param xml: the xml file
  234. :type xml: str()
  235. :rtype: dict() key: href, value: etag
  236. """
  237. #xml.replace('<?xml version="1.0" encoding="utf-8"?>', '')
  238. namespace = "{DAV:}"
  239. element = ET.XML(xml)
  240. abook = dict()
  241. for response in element.iterchildren():
  242. if (response.tag == namespace + "response"):
  243. href = ""
  244. etag = ""
  245. insert = False
  246. for refprop in response.iterchildren():
  247. if (refprop.tag == namespace + "href"):
  248. href = refprop.text
  249. for prop in refprop.iterchildren():
  250. for props in prop.iterchildren():
  251. if (props.tag == namespace + "getcontenttype" and
  252. (props.text == "text/vcard" or
  253. props.text == "text/vcard; charset=utf-8" or
  254. props.text == "text/x-vcard" or
  255. props.text == "text/x-vcard; charset=utf-8")):
  256. insert = True
  257. if (props.tag == namespace + "getetag"):
  258. etag = props.text
  259. if insert:
  260. abook[href] = etag
  261. return abook