filter.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. # This file is part of Radicale - CalDAV and CardDAV 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. import math
  20. import xml.etree.ElementTree as ET
  21. from datetime import date, datetime, timedelta, timezone
  22. from itertools import chain
  23. from typing import (Callable, Iterable, Iterator, List, Optional, Sequence,
  24. Tuple)
  25. import vobject
  26. from radicale import item, xmlutils
  27. from radicale.log import logger
  28. DAY: timedelta = timedelta(days=1)
  29. SECOND: timedelta = timedelta(seconds=1)
  30. DATETIME_MIN: datetime = datetime.min.replace(tzinfo=timezone.utc)
  31. DATETIME_MAX: datetime = datetime.max.replace(tzinfo=timezone.utc)
  32. TIMESTAMP_MIN: int = math.floor(DATETIME_MIN.timestamp())
  33. TIMESTAMP_MAX: int = math.ceil(DATETIME_MAX.timestamp())
  34. def date_to_datetime(d: date) -> datetime:
  35. """Transform any date to a UTC datetime.
  36. If ``d`` is a datetime without timezone, return as UTC datetime. If ``d``
  37. is already a datetime with timezone, return as is.
  38. """
  39. if not isinstance(d, datetime):
  40. d = datetime.combine(d, datetime.min.time())
  41. if not d.tzinfo:
  42. # NOTE: using vobject's UTC as it wasn't playing well with datetime's.
  43. d = d.replace(tzinfo=vobject.icalendar.utc)
  44. return d
  45. def parse_time_range(time_filter: ET.Element) -> Tuple[datetime, datetime]:
  46. start_text = time_filter.get("start")
  47. end_text = time_filter.get("end")
  48. if start_text:
  49. start = datetime.strptime(
  50. start_text, "%Y%m%dT%H%M%SZ").replace(
  51. tzinfo=timezone.utc)
  52. else:
  53. start = DATETIME_MIN
  54. if end_text:
  55. end = datetime.strptime(
  56. end_text, "%Y%m%dT%H%M%SZ").replace(
  57. tzinfo=timezone.utc)
  58. else:
  59. end = DATETIME_MAX
  60. return start, end
  61. def time_range_timestamps(time_filter: ET.Element) -> Tuple[int, int]:
  62. start, end = parse_time_range(time_filter)
  63. return (math.floor(start.timestamp()), math.ceil(end.timestamp()))
  64. def comp_match(item: "item.Item", filter_: ET.Element, level: int = 0) -> bool:
  65. """Check whether the ``item`` matches the comp ``filter_``.
  66. If ``level`` is ``0``, the filter is applied on the
  67. item's collection. Otherwise, it's applied on the item.
  68. See rfc4791-9.7.1.
  69. """
  70. # TODO: Filtering VALARM and VFREEBUSY is not implemented
  71. # HACK: the filters are tested separately against all components
  72. if level == 0:
  73. tag = item.name
  74. elif level == 1:
  75. tag = item.component_name
  76. else:
  77. logger.warning(
  78. "Filters with three levels of comp-filter are not supported")
  79. return True
  80. if not tag:
  81. return False
  82. name = filter_.get("name", "").upper()
  83. if len(filter_) == 0:
  84. # Point #1 of rfc4791-9.7.1
  85. return name == tag
  86. if len(filter_) == 1:
  87. if filter_[0].tag == xmlutils.make_clark("C:is-not-defined"):
  88. # Point #2 of rfc4791-9.7.1
  89. return name != tag
  90. if name != tag:
  91. return False
  92. if (level == 0 and name != "VCALENDAR" or
  93. level == 1 and name not in ("VTODO", "VEVENT", "VJOURNAL")):
  94. logger.warning("Filtering %s is not supported", name)
  95. return True
  96. # Point #3 and #4 of rfc4791-9.7.1
  97. components = ([item.vobject_item] if level == 0
  98. else list(getattr(item.vobject_item,
  99. "%s_list" % tag.lower())))
  100. for child in filter_:
  101. if child.tag == xmlutils.make_clark("C:prop-filter"):
  102. if not any(prop_match(comp, child, "C")
  103. for comp in components):
  104. return False
  105. elif child.tag == xmlutils.make_clark("C:time-range"):
  106. if not time_range_match(item.vobject_item, filter_[0], tag):
  107. return False
  108. elif child.tag == xmlutils.make_clark("C:comp-filter"):
  109. if not comp_match(item, child, level=level + 1):
  110. return False
  111. else:
  112. raise ValueError("Unexpected %r in comp-filter" % child.tag)
  113. return True
  114. def prop_match(vobject_item: vobject.base.Component,
  115. filter_: ET.Element, ns: str) -> bool:
  116. """Check whether the ``item`` matches the prop ``filter_``.
  117. See rfc4791-9.7.2 and rfc6352-10.5.1.
  118. """
  119. name = filter_.get("name", "").lower()
  120. if len(filter_) == 0:
  121. # Point #1 of rfc4791-9.7.2
  122. return name in vobject_item.contents
  123. if len(filter_) == 1:
  124. if filter_[0].tag == xmlutils.make_clark("%s:is-not-defined" % ns):
  125. # Point #2 of rfc4791-9.7.2
  126. return name not in vobject_item.contents
  127. if name not in vobject_item.contents:
  128. return False
  129. # Point #3 and #4 of rfc4791-9.7.2
  130. for child in filter_:
  131. if ns == "C" and child.tag == xmlutils.make_clark("C:time-range"):
  132. if not time_range_match(vobject_item, child, name):
  133. return False
  134. elif child.tag == xmlutils.make_clark("%s:text-match" % ns):
  135. if not text_match(vobject_item, child, name, ns):
  136. return False
  137. elif child.tag == xmlutils.make_clark("%s:param-filter" % ns):
  138. if not param_filter_match(vobject_item, child, name, ns):
  139. return False
  140. else:
  141. raise ValueError("Unexpected %r in prop-filter" % child.tag)
  142. return True
  143. def time_range_match(vobject_item: vobject.base.Component,
  144. filter_: ET.Element, child_name: str) -> bool:
  145. """Check whether the component/property ``child_name`` of
  146. ``vobject_item`` matches the time-range ``filter_``."""
  147. if not filter_.get("start") and not filter_.get("end"):
  148. return False
  149. start, end = parse_time_range(filter_)
  150. matched = False
  151. def range_fn(range_start: datetime, range_end: datetime,
  152. is_recurrence: bool) -> bool:
  153. nonlocal matched
  154. if start < range_end and range_start < end:
  155. matched = True
  156. return True
  157. if end < range_start and not is_recurrence:
  158. return True
  159. return False
  160. def infinity_fn(start: datetime) -> bool:
  161. return False
  162. visit_time_ranges(vobject_item, child_name, range_fn, infinity_fn)
  163. return matched
  164. def time_range_fill(vobject_item: vobject.base.Component,
  165. filter_: ET.Element, child_name: str, n: int = 1
  166. ) -> List[Tuple[datetime, datetime]]:
  167. """Create a list of ``n`` occurances from the component/property ``child_name``
  168. of ``vobject_item``."""
  169. if not filter_.get("start") and not filter_.get("end"):
  170. return []
  171. start, end = parse_time_range(filter_)
  172. ranges: List[Tuple[datetime, datetime]] = []
  173. def range_fn(range_start: datetime, range_end: datetime,
  174. is_recurrence: bool) -> bool:
  175. nonlocal ranges
  176. if start < range_end and range_start < end:
  177. ranges.append((range_start, range_end))
  178. if n > 0 and len(ranges) >= n:
  179. return True
  180. if end < range_start and not is_recurrence:
  181. return True
  182. return False
  183. def infinity_fn(range_start: datetime) -> bool:
  184. return False
  185. visit_time_ranges(vobject_item, child_name, range_fn, infinity_fn)
  186. return ranges
  187. def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
  188. range_fn: Callable[[datetime, datetime, bool], bool],
  189. infinity_fn: Callable[[datetime], bool]) -> None:
  190. """Visit all time ranges in the component/property ``child_name`` of
  191. `vobject_item`` with visitors ``range_fn`` and ``infinity_fn``.
  192. ``range_fn`` gets called for every time_range with ``start`` and ``end``
  193. datetimes and ``is_recurrence`` as arguments. If the function returns True,
  194. the operation is cancelled.
  195. ``infinity_fn`` gets called when an infinite recurrence rule is detected
  196. with ``start`` datetime as argument. If the function returns True, the
  197. operation is cancelled.
  198. See rfc4791-9.9.
  199. """
  200. # HACK: According to rfc5545-3.8.4.4 a recurrence that is rescheduled
  201. # with Recurrence ID affects the recurrence itself and all following
  202. # recurrences too. This is not respected and client don't seem to bother
  203. # either.
  204. def getrruleset(child: vobject.base.Component, ignore: Sequence[date]
  205. ) -> Tuple[Iterable[date], bool]:
  206. infinite = False
  207. for rrule in child.contents.get("rrule", []):
  208. if (";UNTIL=" not in rrule.value.upper() and
  209. ";COUNT=" not in rrule.value.upper()):
  210. infinite = True
  211. break
  212. if infinite:
  213. for dtstart in child.getrruleset(addRDate=True):
  214. if dtstart in ignore:
  215. continue
  216. if infinity_fn(date_to_datetime(dtstart)):
  217. return (), True
  218. break
  219. return filter(lambda dtstart: dtstart not in ignore,
  220. child.getrruleset(addRDate=True)), False
  221. def get_children(components: Iterable[vobject.base.Component]) -> Iterator[
  222. Tuple[vobject.base.Component, bool, List[date]]]:
  223. main = None
  224. rec_main = None
  225. recurrences = []
  226. for comp in components:
  227. if hasattr(comp, "recurrence_id") and comp.recurrence_id.value:
  228. recurrences.append(comp.recurrence_id.value)
  229. if comp.rruleset:
  230. # Prevent possible infinite loop
  231. raise ValueError("Overwritten recurrence with RRULESET")
  232. rec_main = comp
  233. yield comp, True, []
  234. else:
  235. if main is not None:
  236. raise ValueError("Multiple main components. Got comp: {}".format(comp))
  237. main = comp
  238. if main is None and len(recurrences) == 1:
  239. main = rec_main
  240. if main is None:
  241. raise ValueError("Main component missing")
  242. yield main, False, recurrences
  243. # Comments give the lines in the tables of the specification
  244. if child_name == "VEVENT":
  245. for child, is_recurrence, recurrences in get_children(
  246. vobject_item.vevent_list):
  247. # TODO: check if there's a timezone
  248. dtstart = child.dtstart.value
  249. if child.rruleset:
  250. dtstarts, infinity = getrruleset(child, recurrences)
  251. if infinity:
  252. return
  253. else:
  254. dtstarts = (dtstart,)
  255. dtend = getattr(child, "dtend", None)
  256. if dtend is not None:
  257. dtend = dtend.value
  258. original_duration = (dtend - dtstart).total_seconds()
  259. dtend = date_to_datetime(dtend)
  260. duration = getattr(child, "duration", None)
  261. if duration is not None:
  262. original_duration = duration = duration.value
  263. for dtstart in dtstarts:
  264. dtstart_is_datetime = isinstance(dtstart, datetime)
  265. dtstart = date_to_datetime(dtstart)
  266. if dtend is not None:
  267. # Line 1
  268. dtend = dtstart + timedelta(seconds=original_duration)
  269. if range_fn(dtstart, dtend, is_recurrence):
  270. return
  271. elif duration is not None:
  272. if original_duration is None:
  273. original_duration = duration.seconds
  274. if duration.seconds > 0:
  275. # Line 2
  276. if range_fn(dtstart, dtstart + duration,
  277. is_recurrence):
  278. return
  279. else:
  280. # Line 3
  281. if range_fn(dtstart, dtstart + SECOND, is_recurrence):
  282. return
  283. elif dtstart_is_datetime:
  284. # Line 4
  285. if range_fn(dtstart, dtstart + SECOND, is_recurrence):
  286. return
  287. else:
  288. # Line 5
  289. if range_fn(dtstart, dtstart + DAY, is_recurrence):
  290. return
  291. elif child_name == "VTODO":
  292. for child, is_recurrence, recurrences in get_children(
  293. vobject_item.vtodo_list):
  294. dtstart = getattr(child, "dtstart", None)
  295. duration = getattr(child, "duration", None)
  296. due = getattr(child, "due", None)
  297. completed = getattr(child, "completed", None)
  298. created = getattr(child, "created", None)
  299. if dtstart is not None:
  300. dtstart = date_to_datetime(dtstart.value)
  301. if duration is not None:
  302. duration = duration.value
  303. if due is not None:
  304. due = date_to_datetime(due.value)
  305. if dtstart is not None:
  306. original_duration = (due - dtstart).total_seconds()
  307. if completed is not None:
  308. completed = date_to_datetime(completed.value)
  309. if created is not None:
  310. created = date_to_datetime(created.value)
  311. original_duration = (completed - created).total_seconds()
  312. elif created is not None:
  313. created = date_to_datetime(created.value)
  314. if child.rruleset:
  315. reference_dates, infinity = getrruleset(child, recurrences)
  316. if infinity:
  317. return
  318. else:
  319. if dtstart is not None:
  320. reference_dates = (dtstart,)
  321. elif due is not None:
  322. reference_dates = (due,)
  323. elif completed is not None:
  324. reference_dates = (completed,)
  325. elif created is not None:
  326. reference_dates = (created,)
  327. else:
  328. # Line 8
  329. if range_fn(DATETIME_MIN, DATETIME_MAX, is_recurrence):
  330. return
  331. reference_dates = ()
  332. for reference_date in reference_dates:
  333. reference_date = date_to_datetime(reference_date)
  334. if dtstart is not None and duration is not None:
  335. # Line 1
  336. if range_fn(reference_date,
  337. reference_date + duration + SECOND,
  338. is_recurrence):
  339. return
  340. if range_fn(reference_date + duration - SECOND,
  341. reference_date + duration + SECOND,
  342. is_recurrence):
  343. return
  344. elif dtstart is not None and due is not None:
  345. # Line 2
  346. due = reference_date + timedelta(seconds=original_duration)
  347. if (range_fn(reference_date, due, is_recurrence) or
  348. range_fn(reference_date,
  349. reference_date + SECOND, is_recurrence) or
  350. range_fn(due - SECOND, due, is_recurrence) or
  351. range_fn(due - SECOND, reference_date + SECOND,
  352. is_recurrence)):
  353. return
  354. elif dtstart is not None:
  355. if range_fn(reference_date, reference_date + SECOND,
  356. is_recurrence):
  357. return
  358. elif due is not None:
  359. # Line 4
  360. if range_fn(reference_date - SECOND, reference_date,
  361. is_recurrence):
  362. return
  363. elif completed is not None and created is not None:
  364. # Line 5
  365. completed = reference_date + timedelta(
  366. seconds=original_duration)
  367. if (range_fn(reference_date - SECOND,
  368. reference_date + SECOND,
  369. is_recurrence) or
  370. range_fn(completed - SECOND, completed + SECOND,
  371. is_recurrence) or
  372. range_fn(reference_date - SECOND,
  373. reference_date + SECOND, is_recurrence) or
  374. range_fn(completed - SECOND, completed + SECOND,
  375. is_recurrence)):
  376. return
  377. elif completed is not None:
  378. # Line 6
  379. if range_fn(reference_date - SECOND,
  380. reference_date + SECOND, is_recurrence):
  381. return
  382. elif created is not None:
  383. # Line 7
  384. if range_fn(reference_date, DATETIME_MAX, is_recurrence):
  385. return
  386. elif child_name == "VJOURNAL":
  387. for child, is_recurrence, recurrences in get_children(
  388. vobject_item.vjournal_list):
  389. dtstart = getattr(child, "dtstart", None)
  390. if dtstart is not None:
  391. dtstart = dtstart.value
  392. if child.rruleset:
  393. dtstarts, infinity = getrruleset(child, recurrences)
  394. if infinity:
  395. return
  396. else:
  397. dtstarts = (dtstart,)
  398. for dtstart in dtstarts:
  399. dtstart_is_datetime = isinstance(dtstart, datetime)
  400. dtstart = date_to_datetime(dtstart)
  401. if dtstart_is_datetime:
  402. # Line 1
  403. if range_fn(dtstart, dtstart + SECOND, is_recurrence):
  404. return
  405. else:
  406. # Line 2
  407. if range_fn(dtstart, dtstart + DAY, is_recurrence):
  408. return
  409. else:
  410. # Match a property
  411. child = getattr(vobject_item, child_name.lower())
  412. if isinstance(child, date):
  413. child_is_datetime = isinstance(child, datetime)
  414. child = date_to_datetime(child)
  415. if child_is_datetime:
  416. range_fn(child, child + SECOND, False)
  417. else:
  418. range_fn(child, child + DAY, False)
  419. def text_match(vobject_item: vobject.base.Component,
  420. filter_: ET.Element, child_name: str, ns: str,
  421. attrib_name: Optional[str] = None) -> bool:
  422. """Check whether the ``item`` matches the text-match ``filter_``.
  423. See rfc4791-9.7.5.
  424. """
  425. # TODO: collations are not supported, but the default ones needed
  426. # for DAV servers are actually pretty useless. Texts are lowered to
  427. # be case-insensitive, almost as the "i;ascii-casemap" value.
  428. text = next(filter_.itertext()).lower()
  429. match_type = "contains"
  430. if ns == "CR":
  431. match_type = filter_.get("match-type", match_type)
  432. def match(value: str) -> bool:
  433. value = value.lower()
  434. if match_type == "equals":
  435. return value == text
  436. if match_type == "contains":
  437. return text in value
  438. if match_type == "starts-with":
  439. return value.startswith(text)
  440. if match_type == "ends-with":
  441. return value.endswith(text)
  442. raise ValueError("Unexpected text-match match-type: %r" % match_type)
  443. children = getattr(vobject_item, "%s_list" % child_name, [])
  444. if attrib_name is not None:
  445. condition = any(
  446. match(attrib) for child in children
  447. for attrib in child.params.get(attrib_name, []))
  448. else:
  449. res = []
  450. for child in children:
  451. # Some filters such as CATEGORIES provide a list in child.value
  452. if type(child.value) is list:
  453. for value in child.value:
  454. res.append(match(value))
  455. else:
  456. res.append(match(child.value))
  457. condition = any(res)
  458. if filter_.get("negate-condition") == "yes":
  459. return not condition
  460. return condition
  461. def param_filter_match(vobject_item: vobject.base.Component,
  462. filter_: ET.Element, parent_name: str, ns: str) -> bool:
  463. """Check whether the ``item`` matches the param-filter ``filter_``.
  464. See rfc4791-9.7.3.
  465. """
  466. name = filter_.get("name", "").upper()
  467. children = getattr(vobject_item, "%s_list" % parent_name, [])
  468. condition = any(name in child.params for child in children)
  469. if len(filter_) > 0:
  470. if filter_[0].tag == xmlutils.make_clark("%s:text-match" % ns):
  471. return condition and text_match(
  472. vobject_item, filter_[0], parent_name, ns, name)
  473. if filter_[0].tag == xmlutils.make_clark("%s:is-not-defined" % ns):
  474. return not condition
  475. return condition
  476. def simplify_prefilters(filters: Iterable[ET.Element], collection_tag: str
  477. ) -> Tuple[Optional[str], int, int, bool]:
  478. """Creates a simplified condition from ``filters``.
  479. Returns a tuple (``tag``, ``start``, ``end``, ``simple``) where ``tag`` is
  480. a string or None (match all) and ``start`` and ``end`` are POSIX
  481. timestamps (as int). ``simple`` is a bool that indicates that ``filters``
  482. and the simplified condition are identical.
  483. """
  484. flat_filters = list(chain.from_iterable(filters))
  485. simple = len(flat_filters) <= 1
  486. for col_filter in flat_filters:
  487. if collection_tag != "VCALENDAR":
  488. simple = False
  489. break
  490. if (col_filter.tag != xmlutils.make_clark("C:comp-filter") or
  491. col_filter.get("name", "").upper() != "VCALENDAR"):
  492. simple = False
  493. continue
  494. simple &= len(col_filter) <= 1
  495. for comp_filter in col_filter:
  496. if comp_filter.tag != xmlutils.make_clark("C:comp-filter"):
  497. simple = False
  498. continue
  499. tag = comp_filter.get("name", "").upper()
  500. if comp_filter.find(
  501. xmlutils.make_clark("C:is-not-defined")) is not None:
  502. simple = False
  503. continue
  504. simple &= len(comp_filter) <= 1
  505. for time_filter in comp_filter:
  506. if tag not in ("VTODO", "VEVENT", "VJOURNAL"):
  507. simple = False
  508. break
  509. if time_filter.tag != xmlutils.make_clark("C:time-range"):
  510. simple = False
  511. continue
  512. start, end = time_range_timestamps(time_filter)
  513. return tag, start, end, simple
  514. return tag, TIMESTAMP_MIN, TIMESTAMP_MAX, simple
  515. return None, TIMESTAMP_MIN, TIMESTAMP_MAX, simple