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