CalendarQueryValidator.php 12 KB

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