4 * CalendarQuery Validator
6 * This class is responsible for checking if an iCalendar object matches a set
7 * of filters. The main function to do this is 'validate'.
9 * This is used to determine which icalendar objects should be returned for a
10 * calendar-query REPORT request.
14 * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
15 * @author Evert Pot (http://www.rooftopsolutions.nl/)
16 * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
18 class Sabre_CalDAV_CalendarQueryValidator {
21 * Verify if a list of filters applies to the calendar data object
23 * The list of filters must be formatted as parsed by Sabre_CalDAV_CalendarQueryParser
25 * @param Sabre_VObject_Component $vObject
26 * @param array $filters
29 public function validate(Sabre_VObject_Component $vObject,array $filters) {
31 // The top level object is always a component filter.
32 // We'll parse it manually, as it's pretty simple.
33 if ($vObject->name !== $filters['name']) {
38 $this->validateCompFilters($vObject, $filters['comp-filters']) &&
39 $this->validatePropFilters($vObject, $filters['prop-filters']);
45 * This method checks the validity of comp-filters.
47 * A list of comp-filters needs to be specified. Also the parent of the
48 * component we're checking should be specified, not the component to check
51 * @param Sabre_VObject_Component $parent
52 * @param array $filters
55 protected function validateCompFilters(Sabre_VObject_Component $parent, array $filters) {
57 foreach($filters as $filter) {
59 $isDefined = isset($parent->$filter['name']);
61 if ($filter['is-not-defined']) {
74 if ($filter['time-range']) {
75 foreach($parent->$filter['name'] as $subComponent) {
76 if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
83 if (!$filter['comp-filters'] && !$filter['prop-filters']) {
87 // If there are sub-filters, we need to find at least one component
88 // for which the subfilters hold true.
89 foreach($parent->$filter['name'] as $subComponent) {
92 $this->validateCompFilters($subComponent, $filter['comp-filters']) &&
93 $this->validatePropFilters($subComponent, $filter['prop-filters'])) {
94 // We had a match, so this comp-filter succeeds
100 // If we got here it means there were sub-comp-filters or
101 // sub-prop-filters and there was no match. This means this filter
102 // needs to return false.
107 // If we got here it means we got through all comp-filters alive so the
108 // filters were all true.
114 * This method checks the validity of prop-filters.
116 * A list of prop-filters needs to be specified. Also the parent of the
117 * property we're checking should be specified, not the property to check
120 * @param Sabre_VObject_Component $parent
121 * @param array $filters
124 protected function validatePropFilters(Sabre_VObject_Component $parent, array $filters) {
126 foreach($filters as $filter) {
128 $isDefined = isset($parent->$filter['name']);
130 if ($filter['is-not-defined']) {
143 if ($filter['time-range']) {
144 foreach($parent->$filter['name'] as $subComponent) {
145 if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
152 if (!$filter['param-filters'] && !$filter['text-match']) {
156 // If there are sub-filters, we need to find at least one property
157 // for which the subfilters hold true.
158 foreach($parent->$filter['name'] as $subComponent) {
161 $this->validateParamFilters($subComponent, $filter['param-filters']) &&
162 (!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match']))
164 // We had a match, so this prop-filter succeeds
170 // If we got here it means there were sub-param-filters or
171 // text-match filters and there was no match. This means the
172 // filter needs to return false.
177 // If we got here it means we got through all prop-filters alive so the
178 // filters were all true.
184 * This method checks the validity of param-filters.
186 * A list of param-filters needs to be specified. Also the parent of the
187 * parameter we're checking should be specified, not the parameter to check
190 * @param Sabre_VObject_Property $parent
191 * @param array $filters
194 protected function validateParamFilters(Sabre_VObject_Property $parent, array $filters) {
196 foreach($filters as $filter) {
198 $isDefined = isset($parent[$filter['name']]);
200 if ($filter['is-not-defined']) {
213 if (!$filter['text-match']) {
217 // If there are sub-filters, we need to find at least one parameter
218 // for which the subfilters hold true.
219 foreach($parent[$filter['name']] as $subParam) {
221 if($this->validateTextMatch($subParam,$filter['text-match'])) {
222 // We had a match, so this param-filter succeeds
228 // If we got here it means there was a text-match filter and there
229 // were no matches. This means the filter needs to return false.
234 // If we got here it means we got through all param-filters alive so the
235 // filters were all true.
241 * This method checks the validity of a text-match.
243 * A single text-match should be specified as well as the specific property
244 * or parameter we need to validate.
246 * @param Sabre_VObject_Node $parent
247 * @param array $textMatch
250 protected function validateTextMatch(Sabre_VObject_Node $parent, array $textMatch) {
252 $value = (string)$parent;
254 $isMatching = Sabre_DAV_StringUtil::textMatch($value, $textMatch['value'], $textMatch['collation']);
256 return ($textMatch['negate-condition'] xor $isMatching);
261 * Validates if a component matches the given time range.
263 * This is all based on the rules specified in rfc4791, which are quite
266 * @param Sabre_VObject_Node $component
267 * @param DateTime $start
268 * @param DateTime $end
271 protected function validateTimeRange(Sabre_VObject_Node $component, $start, $end) {
273 if (is_null($start)) {
274 $start = new DateTime('1900-01-01');
277 $end = new DateTime('3000-01-01');
280 switch($component->name) {
286 return $component->isInTimeRange($start, $end);
290 // If the valarm is wrapped in a recurring event, we need to
291 // expand the recursions, and validate each.
293 // Our datamodel doesn't easily allow us to do this straight
294 // in the VALARM component code, so this is a hack, and an
295 // expensive one too.
296 if ($component->parent->name === 'VEVENT' && $component->parent->RRULE) {
298 // Fire up the iterator!
299 $it = new Sabre_VObject_RecurrenceIterator($component->parent->parent, (string)$component->parent->UID);
300 while($it->valid()) {
301 $expandedEvent = $it->getEventObject();
303 // We need to check from these expanded alarms, which
304 // one is the first to trigger. Based on this, we can
305 // determine if we can 'give up' expanding events.
307 foreach($expandedEvent->VALARM as $expandedAlarm) {
309 $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
310 if ($expandedAlarm->isInTimeRange($start, $end)) {
314 if ((string)$expandedAlarm->TRIGGER['VALUE'] === 'DATE-TIME') {
315 // This is an alarm with a non-relative trigger
316 // time, likely created by a buggy client. The
317 // implication is that every alarm in this
318 // recurring event trigger at the exact same
319 // time. It doesn't make sense to traverse
322 // We store the first alarm as a means to
323 // figure out when we can stop traversing.
324 if (!$firstAlarm || $effectiveTrigger < $firstAlarm) {
325 $firstAlarm = $effectiveTrigger;
330 if (is_null($firstAlarm)) {
331 // No alarm was found.
333 // Or technically: No alarm that will change for
334 // every instance of the recurrence was found,
335 // which means we can assume there was no match.
338 if ($firstAlarm > $end) {
345 return $component->isInTimeRange($start, $end);
349 throw new Sabre_DAV_Exception_NotImplemented('time-range filters are currently not supported on ' . $component->name . ' components');
357 case 'LAST-MODIFIED' :
358 return ($start <= $component->getDateTime() && $end >= $component->getDateTime());
363 throw new Sabre_DAV_Exception_BadRequest('You cannot create a time-range filter on a ' . $component->name . ' component');