You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

305 lines
9.3 KiB

  1. # Copyright 2019 Therp BV <https://therp.nl>
  2. # Copyright 2019-2020 initOS GmbH <https://initos.com>
  3. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  4. import os
  5. from operator import itemgetter
  6. from urllib.parse import quote_plus
  7. from odoo import api, fields, models, tools
  8. from odoo.tools.safe_eval import safe_eval
  9. import logging
  10. import vobject
  11. # pylint: disable=missing-import-error
  12. from ..controllers.main import PREFIX
  13. from ..radicale.collection import Collection, FileItem, Item
  14. logger = logging.getLogger(__name__)
  15. class DavCollection(models.Model):
  16. _name = "dav.collection"
  17. _description = "A collection accessible via WebDAV"
  18. name = fields.Char(required=True)
  19. rights = fields.Selection(
  20. [
  21. ("owner_only", "Owner Only"),
  22. ("owner_write_only", "Owner Write Only"),
  23. ("authenticated", "Authenticated"),
  24. ],
  25. required=True,
  26. default="owner_only",
  27. )
  28. dav_type = fields.Selection(
  29. [
  30. ("calendar", "Calendar"),
  31. ("addressbook", "Addressbook"),
  32. ("files", "Files"),
  33. ],
  34. string="Type",
  35. required=True,
  36. default="calendar",
  37. )
  38. tag = fields.Char(compute="_compute_tag")
  39. model_id = fields.Many2one(
  40. "ir.model",
  41. string="Model",
  42. required=True,
  43. ondelete="cascade",
  44. domain=[("transient", "=", False)],
  45. )
  46. domain = fields.Char(
  47. required=True,
  48. default="[]",
  49. )
  50. field_uuid = fields.Many2one("ir.model.fields")
  51. field_mapping_ids = fields.One2many(
  52. "dav.collection.field_mapping",
  53. "collection_id",
  54. string="Field mappings",
  55. )
  56. url = fields.Char(compute="_compute_url")
  57. def _compute_tag(self):
  58. for this in self:
  59. if this.dav_type == "calendar":
  60. this.tag = "VCALENDAR"
  61. elif this.dav_type == "addressbook":
  62. this.tag = "VADDRESSBOOK"
  63. def _compute_url(self):
  64. base_url = self.env["ir.config_parameter"].get_param("web.base.url")
  65. for this in self:
  66. this.url = "%s%s/%s/%s" % (
  67. base_url,
  68. PREFIX,
  69. self.env.user.login,
  70. this.id,
  71. )
  72. @api.constrains("domain")
  73. def _check_domain(self):
  74. self._eval_domain()
  75. @api.model
  76. def _eval_context(self):
  77. return {
  78. "user": self.env.user,
  79. }
  80. @api.model
  81. def get_logger(self):
  82. return logger
  83. def _eval_domain(self):
  84. self.ensure_one()
  85. return list(safe_eval(self.domain, self._eval_context()))
  86. def eval(self):
  87. if not self:
  88. return self.env["unknown"]
  89. self.ensure_one()
  90. return self.env[self.model_id.model].search(self._eval_domain())
  91. def get_record(self, components):
  92. self.ensure_one()
  93. collection_model = self.env[self.model_id.model]
  94. field_name = self.field_uuid.name or "id"
  95. domain = [(field_name, "=", components[-1])] + self._eval_domain()
  96. return collection_model.search(domain, limit=1)
  97. def from_vobject(self, item):
  98. self.ensure_one()
  99. result = {}
  100. if self.dav_type == "calendar":
  101. if item.name != "VCALENDAR":
  102. return None
  103. if not hasattr(item, "vevent"):
  104. return None
  105. item = item.vevent
  106. elif self.dav_type == "addressbook" and item.name != "VCARD":
  107. return None
  108. children = {c.name.lower(): c for c in item.getChildren()}
  109. for mapping in self.field_mapping_ids:
  110. name = mapping.name.lower()
  111. if name not in children:
  112. continue
  113. if name in children:
  114. value = mapping.from_vobject(children[name])
  115. if value:
  116. result[mapping.field_id.name] = value
  117. return result
  118. def to_vobject(self, record):
  119. self.ensure_one()
  120. result = None
  121. vobj = None
  122. if self.dav_type == "calendar":
  123. result = vobject.iCalendar()
  124. vobj = result.add("vevent")
  125. if self.dav_type == "addressbook":
  126. result = vobject.vCard()
  127. vobj = result
  128. for mapping in self.field_mapping_ids:
  129. value = mapping.to_vobject(record)
  130. if value:
  131. vobj.add(mapping.name).value = value
  132. if "uid" not in vobj.contents:
  133. vobj.add("uid").value = "%s,%s" % (record._name, record.id)
  134. if "rev" not in vobj.contents and "write_date" in record._fields:
  135. vobj.add("rev").value = record.write_date.strftime("%Y-%m-%dT%H%M%SZ")
  136. return result
  137. @api.model
  138. def _odoo_to_http_datetime(self, value):
  139. return value.strftime("%a, %d %b %Y %H:%M:%S GMT")
  140. @api.model
  141. def _split_path(self, path):
  142. return list(filter(None, os.path.normpath(path or "").strip("/").split("/")))
  143. def dav_list(self, collection, path_components):
  144. self.ensure_one()
  145. if self.dav_type == "files":
  146. if len(path_components) == 3:
  147. collection_model = self.env[self.model_id.model]
  148. record = collection_model.browse(
  149. map(
  150. itemgetter(0),
  151. collection_model.name_search(
  152. path_components[2],
  153. operator="=",
  154. limit=1,
  155. ),
  156. )
  157. )
  158. return [
  159. "/" + "/".join(path_components + [quote_plus(attachment.name)])
  160. for attachment in self.env["ir.attachment"].search(
  161. [
  162. ("type", "=", "binary"),
  163. ("res_model", "=", record._name),
  164. ("res_id", "=", record.id),
  165. ]
  166. )
  167. ]
  168. elif len(path_components) == 2:
  169. return [
  170. "/" + "/".join(path_components + [quote_plus(record.display_name)])
  171. for record in self.eval()
  172. ]
  173. if len(path_components) > 2:
  174. return []
  175. result = []
  176. for record in self.eval():
  177. if self.field_uuid:
  178. uuid = record[self.field_uuid.name]
  179. else:
  180. uuid = str(record.id)
  181. try:
  182. href = "/" + "/".join(path_components + [str(uuid)])
  183. except:
  184. import ipdb
  185. ipdb.set_trace()
  186. result.append(self.dav_get(collection, href))
  187. return result
  188. def dav_delete(self, collection, components):
  189. self.ensure_one()
  190. if self.dav_type == "files":
  191. # TODO: Handle deletion of attachments
  192. pass
  193. else:
  194. self.get_record(components).unlink()
  195. def dav_upload(self, collection, href, item):
  196. self.ensure_one()
  197. components = self._split_path(href)
  198. collection_model = self.env[self.model_id.model]
  199. if self.dav_type == "files":
  200. # TODO: Handle upload of attachments
  201. return None
  202. data = self.from_vobject(item)
  203. record = self.get_record(components)
  204. if not record:
  205. if self.field_uuid:
  206. data[self.field_uuid.name] = components[-1]
  207. record = collection_model.create(data)
  208. uuid = components[-1] if self.field_uuid else record.id
  209. href = "%s/%s" % (href, uuid)
  210. else:
  211. record.write(data)
  212. return Item(
  213. collection=collection,
  214. vobject_item=self.to_vobject(record),
  215. href=href,
  216. last_modified=self._odoo_to_http_datetime(record.write_date),
  217. )
  218. def dav_get(self, collection, href):
  219. self.ensure_one()
  220. components = self._split_path(href)
  221. collection_model = self.env[self.model_id.model]
  222. if self.dav_type == "files":
  223. if len(components) == 3:
  224. result = Collection(href)
  225. result.logger = self.logger
  226. return result
  227. if len(components) == 4:
  228. record = collection_model.browse(
  229. map(
  230. itemgetter(0),
  231. collection_model.name_search(
  232. components[2],
  233. operator="=",
  234. limit=1,
  235. ),
  236. )
  237. )
  238. attachment = self.env["ir.attachment"].search(
  239. [
  240. ("type", "=", "binary"),
  241. ("res_model", "=", record._name),
  242. ("res_id", "=", record.id),
  243. ("name", "=", components[3]),
  244. ],
  245. limit=1,
  246. )
  247. return FileItem(
  248. collection,
  249. item=attachment,
  250. href=href,
  251. last_modified=self._odoo_to_http_datetime(record.write_date),
  252. )
  253. record = self.get_record(components)
  254. if not record:
  255. return None
  256. return Item(
  257. collection=collection,
  258. vobject_item=self.to_vobject(record),
  259. href=href,
  260. last_modified=self._odoo_to_http_datetime(record.write_date),
  261. )