1<?php 2 3namespace Sabre\VObject\Component; 4 5use DateTime; 6use DateTimeZone; 7use Sabre\VObject; 8use Sabre\VObject\Component; 9use Sabre\VObject\Recur\EventIterator; 10use Sabre\VObject\Recur\NoInstancesException; 11 12/** 13 * The VCalendar component 14 * 15 * This component adds functionality to a component, specific for a VCALENDAR. 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 VCalendar extends VObject\Document { 22 23 /** 24 * The default name for this component. 25 * 26 * This should be 'VCALENDAR' or 'VCARD'. 27 * 28 * @var string 29 */ 30 static $defaultName = 'VCALENDAR'; 31 32 /** 33 * This is a list of components, and which classes they should map to. 34 * 35 * @var array 36 */ 37 static $componentMap = array( 38 'VALARM' => 'Sabre\\VObject\\Component\\VAlarm', 39 'VEVENT' => 'Sabre\\VObject\\Component\\VEvent', 40 'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy', 41 'VAVAILABILITY' => 'Sabre\\VObject\\Component\\VAvailability', 42 'AVAILABLE' => 'Sabre\\VObject\\Component\\Available', 43 'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal', 44 'VTIMEZONE' => 'Sabre\\VObject\\Component\\VTimeZone', 45 'VTODO' => 'Sabre\\VObject\\Component\\VTodo', 46 ); 47 48 /** 49 * List of value-types, and which classes they map to. 50 * 51 * @var array 52 */ 53 static $valueMap = array( 54 'BINARY' => 'Sabre\\VObject\\Property\\Binary', 55 'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean', 56 'CAL-ADDRESS' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', 57 'DATE' => 'Sabre\\VObject\\Property\\ICalendar\\Date', 58 'DATE-TIME' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 59 'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', 60 'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue', 61 'INTEGER' => 'Sabre\\VObject\\Property\\IntegerValue', 62 'PERIOD' => 'Sabre\\VObject\\Property\\ICalendar\\Period', 63 'RECUR' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', 64 'TEXT' => 'Sabre\\VObject\\Property\\Text', 65 'TIME' => 'Sabre\\VObject\\Property\\Time', 66 'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only. 67 'URI' => 'Sabre\\VObject\\Property\\Uri', 68 'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset', 69 ); 70 71 /** 72 * List of properties, and which classes they map to. 73 * 74 * @var array 75 */ 76 static $propertyMap = array( 77 // Calendar properties 78 'CALSCALE' => 'Sabre\\VObject\\Property\\FlatText', 79 'METHOD' => 'Sabre\\VObject\\Property\\FlatText', 80 'PRODID' => 'Sabre\\VObject\\Property\\FlatText', 81 'VERSION' => 'Sabre\\VObject\\Property\\FlatText', 82 83 // Component properties 84 'ATTACH' => 'Sabre\\VObject\\Property\\Uri', 85 'CATEGORIES' => 'Sabre\\VObject\\Property\\Text', 86 'CLASS' => 'Sabre\\VObject\\Property\\FlatText', 87 'COMMENT' => 'Sabre\\VObject\\Property\\FlatText', 88 'DESCRIPTION' => 'Sabre\\VObject\\Property\\FlatText', 89 'GEO' => 'Sabre\\VObject\\Property\\FloatValue', 90 'LOCATION' => 'Sabre\\VObject\\Property\\FlatText', 91 'PERCENT-COMPLETE' => 'Sabre\\VObject\\Property\\IntegerValue', 92 'PRIORITY' => 'Sabre\\VObject\\Property\\IntegerValue', 93 'RESOURCES' => 'Sabre\\VObject\\Property\\Text', 94 'STATUS' => 'Sabre\\VObject\\Property\\FlatText', 95 'SUMMARY' => 'Sabre\\VObject\\Property\\FlatText', 96 97 // Date and Time Component Properties 98 'COMPLETED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 99 'DTEND' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 100 'DUE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 101 'DTSTART' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 102 'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', 103 'FREEBUSY' => 'Sabre\\VObject\\Property\\ICalendar\\Period', 104 'TRANSP' => 'Sabre\\VObject\\Property\\FlatText', 105 106 // Time Zone Component Properties 107 'TZID' => 'Sabre\\VObject\\Property\\FlatText', 108 'TZNAME' => 'Sabre\\VObject\\Property\\FlatText', 109 'TZOFFSETFROM' => 'Sabre\\VObject\\Property\\UtcOffset', 110 'TZOFFSETTO' => 'Sabre\\VObject\\Property\\UtcOffset', 111 'TZURL' => 'Sabre\\VObject\\Property\\Uri', 112 113 // Relationship Component Properties 114 'ATTENDEE' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', 115 'CONTACT' => 'Sabre\\VObject\\Property\\FlatText', 116 'ORGANIZER' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', 117 'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 118 'RELATED-TO' => 'Sabre\\VObject\\Property\\FlatText', 119 'URL' => 'Sabre\\VObject\\Property\\Uri', 120 'UID' => 'Sabre\\VObject\\Property\\FlatText', 121 122 // Recurrence Component Properties 123 'EXDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 124 'RDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 125 'RRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', 126 'EXRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', // Deprecated since rfc5545 127 128 // Alarm Component Properties 129 'ACTION' => 'Sabre\\VObject\\Property\\FlatText', 130 'REPEAT' => 'Sabre\\VObject\\Property\\IntegerValue', 131 'TRIGGER' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', 132 133 // Change Management Component Properties 134 'CREATED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 135 'DTSTAMP' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 136 'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 137 'SEQUENCE' => 'Sabre\\VObject\\Property\\IntegerValue', 138 139 // Request Status 140 'REQUEST-STATUS' => 'Sabre\\VObject\\Property\\Text', 141 142 // Additions from draft-daboo-valarm-extensions-04 143 'ALARM-AGENT' => 'Sabre\\VObject\\Property\\Text', 144 'ACKNOWLEDGED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', 145 'PROXIMITY' => 'Sabre\\VObject\\Property\\Text', 146 'DEFAULT-ALARM' => 'Sabre\\VObject\\Property\\Boolean', 147 148 // Additions from draft-daboo-calendar-availability-05 149 'BUSYTYPE' => 'Sabre\\VObject\\Property\\Text', 150 151 ); 152 153 /** 154 * Returns the current document type. 155 * 156 * @return void 157 */ 158 function getDocumentType() { 159 160 return self::ICALENDAR20; 161 162 } 163 164 /** 165 * Returns a list of all 'base components'. For instance, if an Event has 166 * a recurrence rule, and one instance is overridden, the overridden event 167 * will have the same UID, but will be excluded from this list. 168 * 169 * VTIMEZONE components will always be excluded. 170 * 171 * @param string $componentName filter by component name 172 * @return VObject\Component[] 173 */ 174 function getBaseComponents($componentName = null) { 175 176 $components = array(); 177 foreach($this->children as $component) { 178 179 if (!$component instanceof VObject\Component) 180 continue; 181 182 if (isset($component->{'RECURRENCE-ID'})) 183 continue; 184 185 if ($componentName && $component->name !== strtoupper($componentName)) 186 continue; 187 188 if ($component->name === 'VTIMEZONE') 189 continue; 190 191 $components[] = $component; 192 193 } 194 195 return $components; 196 197 } 198 199 /** 200 * Returns the first component that is not a VTIMEZONE, and does not have 201 * an RECURRENCE-ID. 202 * 203 * If there is no such component, null will be returned. 204 * 205 * @param string $componentName filter by component name 206 * @return VObject\Component|null 207 */ 208 function getBaseComponent($componentName = null) { 209 210 foreach($this->children as $component) { 211 212 if (!$component instanceof VObject\Component) 213 continue; 214 215 if (isset($component->{'RECURRENCE-ID'})) 216 continue; 217 218 if ($componentName && $component->name !== strtoupper($componentName)) 219 continue; 220 221 if ($component->name === 'VTIMEZONE') 222 continue; 223 224 return $component; 225 226 } 227 228 } 229 230 /** 231 * If this calendar object, has events with recurrence rules, this method 232 * can be used to expand the event into multiple sub-events. 233 * 234 * Each event will be stripped from it's recurrence information, and only 235 * the instances of the event in the specified timerange will be left 236 * alone. 237 * 238 * In addition, this method will cause timezone information to be stripped, 239 * and normalized to UTC. 240 * 241 * This method will alter the VCalendar. This cannot be reversed. 242 * 243 * This functionality is specifically used by the CalDAV standard. It is 244 * possible for clients to request expand events, if they are rather simple 245 * clients and do not have the possibility to calculate recurrences. 246 * 247 * @param DateTime $start 248 * @param DateTime $end 249 * @param DateTimeZone $timeZone reference timezone for floating dates and 250 * times. 251 * @return void 252 */ 253 function expand(DateTime $start, DateTime $end, DateTimeZone $timeZone = null) { 254 255 $newEvents = array(); 256 257 if (!$timeZone) { 258 $timeZone = new DateTimeZone('UTC'); 259 } 260 261 // An array of events. Events are indexed by UID. Each item in this 262 // array is a list of one or more events that match the UID. 263 $recurringEvents = array(); 264 265 foreach($this->select('VEVENT') as $key=>$vevent) { 266 267 $uid = (string)$vevent->UID; 268 if (!$uid) { 269 throw new \LogicException('Event did not have a UID!'); 270 } 271 272 if (isset($vevent->{'RECURRENCE-ID'}) || isset($vevent->RRULE)) { 273 if (isset($recurringEvents[$uid])) { 274 $recurringEvents[$uid][] = $vevent; 275 } else { 276 $recurringEvents[$uid] = array($vevent); 277 } 278 continue; 279 } 280 281 if (!isset($vevent->RRULE)) { 282 if ($vevent->isInTimeRange($start, $end)) { 283 $newEvents[] = $vevent; 284 } 285 continue; 286 } 287 288 } 289 290 foreach($recurringEvents as $events) { 291 292 try { 293 $it = new EventIterator($events, $timeZone); 294 295 } catch (NoInstancesException $e) { 296 // This event is recurring, but it doesn't have a single 297 // instance. We are skipping this event from the output 298 // entirely. 299 continue; 300 } 301 $it->fastForward($start); 302 303 while($it->valid() && $it->getDTStart() < $end) { 304 305 if ($it->getDTEnd() > $start) { 306 307 $newEvents[] = $it->getEventObject(); 308 309 } 310 $it->next(); 311 312 } 313 314 } 315 316 // Wiping out all old VEVENT objects 317 unset($this->VEVENT); 318 319 // Setting all properties to UTC time. 320 foreach($newEvents as $newEvent) { 321 322 foreach($newEvent->children as $child) { 323 if ($child instanceof VObject\Property\ICalendar\DateTime && $child->hasTime()) { 324 $dt = $child->getDateTimes($timeZone); 325 // We only need to update the first timezone, because 326 // setDateTimes will match all other timezones to the 327 // first. 328 $dt[0]->setTimeZone(new DateTimeZone('UTC')); 329 $child->setDateTimes($dt); 330 } 331 332 } 333 $this->add($newEvent); 334 335 } 336 337 // Removing all VTIMEZONE components 338 unset($this->VTIMEZONE); 339 340 } 341 342 /** 343 * This method should return a list of default property values. 344 * 345 * @return array 346 */ 347 protected function getDefaults() { 348 349 return array( 350 'VERSION' => '2.0', 351 'PRODID' => '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN', 352 'CALSCALE' => 'GREGORIAN', 353 ); 354 355 } 356 357 /** 358 * A simple list of validation rules. 359 * 360 * This is simply a list of properties, and how many times they either 361 * must or must not appear. 362 * 363 * Possible values per property: 364 * * 0 - Must not appear. 365 * * 1 - Must appear exactly once. 366 * * + - Must appear at least once. 367 * * * - Can appear any number of times. 368 * * ? - May appear, but not more than once. 369 * 370 * @var array 371 */ 372 function getValidationRules() { 373 374 return array( 375 'PRODID' => 1, 376 'VERSION' => 1, 377 378 'CALSCALE' => '?', 379 'METHOD' => '?', 380 ); 381 382 } 383 384 /** 385 * Validates the node for correctness. 386 * 387 * The following options are supported: 388 * Node::REPAIR - May attempt to automatically repair the problem. 389 * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. 390 * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. 391 * 392 * This method returns an array with detected problems. 393 * Every element has the following properties: 394 * 395 * * level - problem level. 396 * * message - A human-readable string describing the issue. 397 * * node - A reference to the problematic node. 398 * 399 * The level means: 400 * 1 - The issue was repaired (only happens if REPAIR was turned on). 401 * 2 - A warning. 402 * 3 - An error. 403 * 404 * @param int $options 405 * @return array 406 */ 407 function validate($options = 0) { 408 409 $warnings = parent::validate($options); 410 411 if ($ver = $this->VERSION) { 412 if ((string)$ver !== '2.0') { 413 $warnings[] = array( 414 'level' => 3, 415 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', 416 'node' => $this, 417 ); 418 } 419 420 } 421 422 $uidList = array(); 423 424 $componentsFound = 0; 425 426 $componentTypes = array(); 427 428 foreach($this->children as $child) { 429 if($child instanceof Component) { 430 $componentsFound++; 431 432 if (!in_array($child->name, array('VEVENT', 'VTODO', 'VJOURNAL'))) { 433 continue; 434 } 435 $componentTypes[] = $child->name; 436 437 $uid = (string)$child->UID; 438 $isMaster = isset($child->{'RECURRENCE-ID'})?0:1; 439 if (isset($uidList[$uid])) { 440 $uidList[$uid]['count']++; 441 if ($isMaster && $uidList[$uid]['hasMaster']) { 442 $warnings[] = array( 443 'level' => 3, 444 'message' => 'More than one master object was found for the object with UID ' . $uid, 445 'node' => $this, 446 ); 447 } 448 $uidList[$uid]['hasMaster']+=$isMaster; 449 } else { 450 $uidList[$uid] = array( 451 'count' => 1, 452 'hasMaster' => $isMaster, 453 ); 454 } 455 456 } 457 } 458 459 if ($componentsFound===0) { 460 $warnings[] = array( 461 'level' => 3, 462 'message' => 'An iCalendar object must have at least 1 component.', 463 'node' => $this, 464 ); 465 } 466 467 if ($options & self::PROFILE_CALDAV) { 468 if (count($uidList)>1) { 469 $warnings[] = array( 470 'level' => 3, 471 'message' => 'A calendar object on a CalDAV server may only have components with the same UID.', 472 'node' => $this, 473 ); 474 } 475 if (count(array_unique($componentTypes))===0) { 476 $warnings[] = array( 477 'level' => 3, 478 'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).', 479 'node' => $this, 480 ); 481 } 482 if (count(array_unique($componentTypes))>1) { 483 $warnings[] = array( 484 'level' => 3, 485 'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).', 486 'node' => $this, 487 ); 488 } 489 490 if (isset($this->METHOD)) { 491 $warnings[] = array( 492 'level' => 3, 493 'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.', 494 'node' => $this, 495 ); 496 } 497 } 498 499 return $warnings; 500 501 } 502 503 /** 504 * Returns all components with a specific UID value. 505 * 506 * @return array 507 */ 508 function getByUID($uid) { 509 510 return array_filter($this->children, function($item) use ($uid) { 511 512 if (!$item instanceof Component) { 513 return false; 514 } 515 if (!$itemUid = $item->select('UID')) { 516 return false; 517 } 518 $itemUid = current($itemUid)->getValue(); 519 return $uid === $itemUid; 520 521 }); 522 523 } 524 525 526} 527