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