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) 2011-2015 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\\Float',
61        'INTEGER'          => 'Sabre\\VObject\\Property\\Integer',
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\\Float',
90        'LOCATION'          => 'Sabre\\VObject\\Property\\FlatText',
91        'PERCENT-COMPLETE'  => 'Sabre\\VObject\\Property\\Integer',
92        'PRIORITY'          => 'Sabre\\VObject\\Property\\Integer',
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\\Integer',
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\\Integer',
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