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