CalendarQueryValidator.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sabre\CalDAV;
  4. use DateTime;
  5. use Sabre\VObject;
  6. /**
  7. * CalendarQuery Validator.
  8. *
  9. * This class is responsible for checking if an iCalendar object matches a set
  10. * of filters. The main function to do this is 'validate'.
  11. *
  12. * This is used to determine which icalendar objects should be returned for a
  13. * calendar-query REPORT request.
  14. *
  15. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  16. * @author Evert Pot (http://evertpot.com/)
  17. * @license http://sabre.io/license/ Modified BSD License
  18. */
  19. class CalendarQueryValidator
  20. {
  21. /**
  22. * Verify if a list of filters applies to the calendar data object.
  23. *
  24. * The list of filters must be formatted as parsed by \Sabre\CalDAV\CalendarQueryParser
  25. *
  26. * @return bool
  27. */
  28. public function validate(VObject\Component\VCalendar $vObject, array $filters)
  29. {
  30. // The top level object is always a component filter.
  31. // We'll parse it manually, as it's pretty simple.
  32. if ($vObject->name !== $filters['name']) {
  33. return false;
  34. }
  35. return
  36. $this->validateCompFilters($vObject, $filters['comp-filters']) &&
  37. $this->validatePropFilters($vObject, $filters['prop-filters']);
  38. }
  39. /**
  40. * This method checks the validity of comp-filters.
  41. *
  42. * A list of comp-filters needs to be specified. Also the parent of the
  43. * component we're checking should be specified, not the component to check
  44. * itself.
  45. *
  46. * @return bool
  47. */
  48. protected function validateCompFilters(VObject\Component $parent, array $filters)
  49. {
  50. foreach ($filters as $filter) {
  51. $isDefined = isset($parent->{$filter['name']});
  52. if ($filter['is-not-defined']) {
  53. if ($isDefined) {
  54. return false;
  55. } else {
  56. continue;
  57. }
  58. }
  59. if (!$isDefined) {
  60. return false;
  61. }
  62. if (array_key_exists('time-range', $filter) && $filter['time-range']) {
  63. foreach ($parent->{$filter['name']} as $subComponent) {
  64. $start = null;
  65. $end = null;
  66. if (array_key_exists('start', $filter['time-range'])) {
  67. $start = $filter['time-range']['start'];
  68. }
  69. if (array_key_exists('end', $filter['time-range'])) {
  70. $end = $filter['time-range']['end'];
  71. }
  72. if ($this->validateTimeRange($subComponent, $start, $end)) {
  73. continue 2;
  74. }
  75. }
  76. return false;
  77. }
  78. if (!$filter['comp-filters'] && !$filter['prop-filters']) {
  79. continue;
  80. }
  81. // If there are sub-filters, we need to find at least one component
  82. // for which the subfilters hold true.
  83. foreach ($parent->{$filter['name']} as $subComponent) {
  84. if (
  85. $this->validateCompFilters($subComponent, $filter['comp-filters']) &&
  86. $this->validatePropFilters($subComponent, $filter['prop-filters'])) {
  87. // We had a match, so this comp-filter succeeds
  88. continue 2;
  89. }
  90. }
  91. // If we got here it means there were sub-comp-filters or
  92. // sub-prop-filters and there was no match. This means this filter
  93. // needs to return false.
  94. return false;
  95. }
  96. // If we got here it means we got through all comp-filters alive so the
  97. // filters were all true.
  98. return true;
  99. }
  100. /**
  101. * This method checks the validity of prop-filters.
  102. *
  103. * A list of prop-filters needs to be specified. Also the parent of the
  104. * property we're checking should be specified, not the property to check
  105. * itself.
  106. *
  107. * @return bool
  108. */
  109. protected function validatePropFilters(VObject\Component $parent, array $filters)
  110. {
  111. foreach ($filters as $filter) {
  112. $isDefined = isset($parent->{$filter['name']});
  113. if ($filter['is-not-defined']) {
  114. if ($isDefined) {
  115. return false;
  116. } else {
  117. continue;
  118. }
  119. }
  120. if (!$isDefined) {
  121. return false;
  122. }
  123. if (array_key_exists('time-range', $filter) && $filter['time-range']) {
  124. foreach ($parent->{$filter['name']} as $subComponent) {
  125. $start = null;
  126. $end = null;
  127. if (array_key_exists('start', $filter['time-range'])) {
  128. $start = $filter['time-range']['start'];
  129. }
  130. if (array_key_exists('end', $filter['time-range'])) {
  131. $end = $filter['time-range']['end'];
  132. }
  133. if ($this->validateTimeRange($subComponent, $start, $end)) {
  134. continue 2;
  135. }
  136. }
  137. return false;
  138. }
  139. if (!$filter['param-filters'] && !$filter['text-match']) {
  140. continue;
  141. }
  142. // If there are sub-filters, we need to find at least one property
  143. // for which the subfilters hold true.
  144. foreach ($parent->{$filter['name']} as $subComponent) {
  145. if (
  146. $this->validateParamFilters($subComponent, $filter['param-filters']) &&
  147. (!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match']))
  148. ) {
  149. // We had a match, so this prop-filter succeeds
  150. continue 2;
  151. }
  152. }
  153. // If we got here it means there were sub-param-filters or
  154. // text-match filters and there was no match. This means the
  155. // filter needs to return false.
  156. return false;
  157. }
  158. // If we got here it means we got through all prop-filters alive so the
  159. // filters were all true.
  160. return true;
  161. }
  162. /**
  163. * This method checks the validity of param-filters.
  164. *
  165. * A list of param-filters needs to be specified. Also the parent of the
  166. * parameter we're checking should be specified, not the parameter to check
  167. * itself.
  168. *
  169. * @return bool
  170. */
  171. protected function validateParamFilters(VObject\Property $parent, array $filters)
  172. {
  173. foreach ($filters as $filter) {
  174. $isDefined = isset($parent[$filter['name']]);
  175. if ($filter['is-not-defined']) {
  176. if ($isDefined) {
  177. return false;
  178. } else {
  179. continue;
  180. }
  181. }
  182. if (!$isDefined) {
  183. return false;
  184. }
  185. if (!$filter['text-match']) {
  186. continue;
  187. }
  188. // If there are sub-filters, we need to find at least one parameter
  189. // for which the subfilters hold true.
  190. foreach ($parent[$filter['name']]->getParts() as $paramPart) {
  191. if ($this->validateTextMatch($paramPart, $filter['text-match'])) {
  192. // We had a match, so this param-filter succeeds
  193. continue 2;
  194. }
  195. }
  196. // If we got here it means there was a text-match filter and there
  197. // were no matches. This means the filter needs to return false.
  198. return false;
  199. }
  200. // If we got here it means we got through all param-filters alive so the
  201. // filters were all true.
  202. return true;
  203. }
  204. /**
  205. * This method checks the validity of a text-match.
  206. *
  207. * A single text-match should be specified as well as the specific property
  208. * or parameter we need to validate.
  209. *
  210. * @param VObject\Node|string $check value to check against
  211. *
  212. * @return bool
  213. */
  214. protected function validateTextMatch($check, array $textMatch)
  215. {
  216. if ($check instanceof VObject\Node) {
  217. $check = $check->getValue();
  218. }
  219. $isMatching = \Sabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']);
  220. return $textMatch['negate-condition'] xor $isMatching;
  221. }
  222. /**
  223. * Validates if a component matches the given time range.
  224. *
  225. * This is all based on the rules specified in rfc4791, which are quite
  226. * complex.
  227. *
  228. * @param DateTime $start
  229. * @param DateTime $end
  230. *
  231. * @return bool
  232. */
  233. protected function validateTimeRange(VObject\Node $component, $start, $end)
  234. {
  235. if (is_null($start)) {
  236. $start = new DateTime('1900-01-01');
  237. }
  238. if (is_null($end)) {
  239. $end = new DateTime('3000-01-01');
  240. }
  241. switch ($component->name) {
  242. case 'VEVENT':
  243. case 'VTODO':
  244. case 'VJOURNAL':
  245. return $component->isInTimeRange($start, $end);
  246. case 'VALARM':
  247. // If the valarm is wrapped in a recurring event, we need to
  248. // expand the recursions, and validate each.
  249. //
  250. // Our datamodel doesn't easily allow us to do this straight
  251. // in the VALARM component code, so this is a hack, and an
  252. // expensive one too.
  253. if ('VEVENT' === $component->parent->name && $component->parent->RRULE) {
  254. // Fire up the iterator!
  255. $it = new VObject\Recur\EventIterator($component->parent->parent, (string) $component->parent->UID);
  256. while ($it->valid()) {
  257. $expandedEvent = $it->getEventObject();
  258. // We need to check from these expanded alarms, which
  259. // one is the first to trigger. Based on this, we can
  260. // determine if we can 'give up' expanding events.
  261. $firstAlarm = null;
  262. if (null !== $expandedEvent->VALARM) {
  263. foreach ($expandedEvent->VALARM as $expandedAlarm) {
  264. $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
  265. if ($expandedAlarm->isInTimeRange($start, $end)) {
  266. return true;
  267. }
  268. if ('DATE-TIME' === (string) $expandedAlarm->TRIGGER['VALUE']) {
  269. // This is an alarm with a non-relative trigger
  270. // time, likely created by a buggy client. The
  271. // implication is that every alarm in this
  272. // recurring event trigger at the exact same
  273. // time. It doesn't make sense to traverse
  274. // further.
  275. } else {
  276. // We store the first alarm as a means to
  277. // figure out when we can stop traversing.
  278. if (!$firstAlarm || $effectiveTrigger < $firstAlarm) {
  279. $firstAlarm = $effectiveTrigger;
  280. }
  281. }
  282. }
  283. }
  284. if (is_null($firstAlarm)) {
  285. // No alarm was found.
  286. //
  287. // Or technically: No alarm that will change for
  288. // every instance of the recurrence was found,
  289. // which means we can assume there was no match.
  290. return false;
  291. }
  292. if ($firstAlarm > $end) {
  293. return false;
  294. }
  295. $it->next();
  296. }
  297. return false;
  298. } else {
  299. return $component->isInTimeRange($start, $end);
  300. }
  301. // no break
  302. case 'VFREEBUSY':
  303. throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on '.$component->name.' components');
  304. case 'COMPLETED':
  305. case 'CREATED':
  306. case 'DTEND':
  307. case 'DTSTAMP':
  308. case 'DTSTART':
  309. case 'DUE':
  310. case 'LAST-MODIFIED':
  311. return $start <= $component->getDateTime() && $end >= $component->getDateTime();
  312. default:
  313. throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a '.$component->name.' component');
  314. }
  315. }
  316. }