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