1<?php
2/**
3 * Kronolith_Event defines a generic API for events.
4 *
5 * Copyright 1999-2017 Horde LLC (http://www.horde.org/)
6 *
7 * See the enclosed file COPYING for license information (GPL). If you
8 * did not receive this file, see http://www.horde.org/licenses/gpl.
9 *
10 * @author  Chuck Hagenbuch <chuck@horde.org>
11 * @author  Jan Schneider <jan@horde.org>
12 * @package Kronolith
13 */
14abstract class Kronolith_Event
15{
16    /**
17     * Flag that is set to true if this event has data from either a storage
18     * backend or a form or other import method.
19     *
20     * @var boolean
21     */
22    public $initialized = false;
23
24    /**
25     * Flag that is set to true if this event exists in a storage driver.
26     *
27     * @var boolean
28     */
29    public $stored = false;
30
31    /**
32     * The driver unique identifier for this event.
33     *
34     * @var string
35     */
36    protected $_id = null;
37
38    /**
39     * The UID for this event.
40     *
41     * @var string
42     */
43    public $uid = null;
44
45    /**
46     * The iCalendar SEQUENCE for this event.
47     *
48     * @var integer
49     */
50    public $sequence = null;
51
52    /**
53     * The user id of the creator of the event.
54     *
55     * @var string
56     */
57    protected $_creator = null;
58
59    /**
60     * The title of this event.
61     *
62     * For displaying in the interface use getTitle() instead.
63     *
64     * @var string
65     */
66    public $title = '';
67
68    /**
69     * The location this event occurs at.
70     *
71     * @var string
72     */
73    public $location = '';
74
75    /**
76     * The timezone of this event.
77     *
78     * @var string
79     */
80    public $timezone;
81
82    /**
83     * The status of this event.
84     *
85     * @var integer
86     */
87    public $status = Kronolith::STATUS_CONFIRMED;
88
89    /**
90     * URL to an icon of this event.
91     *
92     * @var string
93     */
94    public $icon = '';
95
96    /**
97     * The description for this event.
98     *
99     * @var string
100     */
101    public $description = '';
102
103    /**
104     * URL of this event.
105     *
106     * @var string
107     */
108    public $url = '';
109
110    /**
111     * Whether the event is private.
112     *
113     * @var boolean
114     */
115    public $private = false;
116
117    /**
118     * Event tags from the storage backend (e.g. Kolab)
119     *
120     * @var array
121     */
122    protected $_internaltags;
123
124    /**
125     * This tag's events.
126     *
127     * @var array|string
128     */
129    protected $_tags = null;
130
131    /**
132     * Geolocation
133     *
134     * @var array
135     */
136    protected $_geoLocation;
137
138    /**
139     * Whether this is the event on the first day of a multi-day event.
140     *
141     * @var boolen
142     */
143    public $first = true;
144
145    /**
146     * Whether this is the event on the last day of a multi-day event.
147     *
148     * @var boolen
149     */
150    public $last = true;
151
152    /**
153     * All the attendees of this event.
154     *
155     * This is an associative array where the keys are the email addresses
156     * of the attendees, and the values are also associative arrays with
157     * keys 'attendance' and 'response' pointing to the attendees' attendance
158     * and response values, respectively.
159     *
160     * @var array
161     */
162    public $attendees = array();
163
164    /**
165     * All resources of this event.
166     *
167     * This is an associative array where keys are resource uids, values are
168     * associative arrays with keys attendance and response.
169     *
170     * @var array
171     */
172    protected $_resources = array();
173
174    /**
175     * The start time of the event.
176     *
177     * @var Horde_Date
178     */
179    public $start;
180
181    /**
182     * The end time of the event.
183     *
184     * @var Horde_Date
185     */
186    public $end;
187
188    /**
189     * The original start time of the event.
190     *
191     * This may differ from $start on multi-day events where $start is the
192     * start time on the current day. For recurring events this is the start
193     * time of the current recurrence.
194     *
195     * @var Horde_Date
196     */
197    protected $_originalStart;
198
199    /**
200     * The original end time of the event.
201     *
202     * @see $_originalStart for details.
203     *
204     * @var Horde_Date
205     */
206    protected $_originalEnd;
207
208    /**
209     * The duration of this event in minutes
210     *
211     * @var integer
212     */
213    public $durMin = 0;
214
215    /**
216     * Whether this is an all-day event.
217     *
218     * @var boolean
219     */
220    public $allday = false;
221
222    /**
223     * The creation time.
224     *
225     * @see loadHistory()
226     * @var Horde_Date
227     */
228    public $created;
229
230    /**
231     * The creator string.
232     *
233     * @see loadHistory()
234     * @var string
235     */
236    public $createdby;
237
238    /**
239     * The last modification time.
240     *
241     * @see loadHistory()
242     * @var Horde_Date
243     */
244    public $modified;
245
246    /**
247     * The last-modifier string.
248     *
249     * @see loadHistory()
250     * @var string
251     */
252    public $modifiedby;
253
254    /**
255     * Number of minutes before the event starts to trigger an alarm.
256     *
257     * @var integer
258     */
259    public $alarm = 0;
260
261    /**
262     * Snooze minutes for this event's alarm.
263     *
264     * @see Horde_Alarm::snooze()
265     *
266     * @var integer
267     */
268    protected $_snooze;
269
270    /**
271     * The particular alarm methods overridden for this event.
272     *
273     * @var array
274     */
275    public $methods;
276
277    /**
278     * The identifier of the calender this event exists on.
279     *
280     * @var string
281     */
282    public $calendar;
283
284    /**
285     * The type of the calender this event exists on.
286     *
287     * @var string
288     */
289    public $calendarType;
290
291    /**
292     * The HTML background color to be used for this event.
293     *
294     * @var string
295     */
296    protected $_backgroundColor = '#dddddd';
297
298    /**
299     * The HTML foreground color to be used for this event.
300     *
301     * @var string
302     */
303    protected $_foregroundColor = '#000000';
304
305    /**
306     * The VarRenderer class to use for printing select elements.
307     *
308     * @var Horde_Core_Ui_VarRenderer
309     */
310    private $_varRenderer;
311
312    /**
313     * The Horde_Date_Recurrence class for this event.
314     *
315     * @var Horde_Date_Recurrence
316     */
317    public $recurrence;
318
319    /**
320     * Used in view renderers.
321     *
322     * @var integer
323     */
324    protected $_overlap;
325
326    /**
327     * Used in view renderers.
328     *
329     * @var integer
330     */
331    protected $_indent;
332
333    /**
334     * Used in view renderers.
335     *
336     * @var integer
337     */
338    protected $_span;
339
340    /**
341     * Used in view renderers.
342     *
343     * @var integer
344     */
345    protected $_rowspan;
346
347    /**
348     * The baseid. For events that represent exceptions this is the UID of the
349     * original, recurring event.
350     *
351     * @var string
352     */
353    public $baseid;
354
355    /**
356     * For exceptions, the date of the original recurring event that this is an
357     * exception for.
358     *
359     * @var Horde_Date
360     */
361    public $exceptionoriginaldate;
362
363    /**
364     * The cached event duration, split up in time units.
365     *
366     * @see getDuration()
367     * @var stdClass
368     */
369    protected $_duration;
370
371    /**
372     * Constructor.
373     *
374     * @param Kronolith_Driver $driver  The backend driver that this event is
375     *                                  stored in.
376     * @param mixed $eventObject        Backend specific event object
377     *                                  that this will represent.
378     */
379    public function __construct(Kronolith_Driver $driver, $eventObject = null)
380    {
381        $this->calendar = $driver->calendar;
382        list($this->_backgroundColor, $this->_foregroundColor) = $driver->colors();
383
384        if (!is_null($eventObject)) {
385            $this->fromDriver($eventObject);
386        }
387    }
388
389    /**
390     * Retrieves history information for this event from the history backend.
391     */
392    public function loadHistory()
393    {
394        try {
395            $log = $GLOBALS['injector']->getInstance('Horde_History')
396                ->getHistory('kronolith:' . $this->calendar . ':' . $this->uid);
397            $userId = $GLOBALS['registry']->getAuth();
398            foreach ($log as $entry) {
399                switch ($entry['action']) {
400                case 'add':
401                    $this->created = new Horde_Date($entry['ts']);
402                    if ($userId != $entry['who']) {
403                        $this->createdby = sprintf(_("by %s"), Kronolith::getUserName($entry['who']));
404                    } else {
405                        $this->createdby = _("by me");
406                    }
407                    break;
408
409                case 'modify':
410                    if ($this->modified &&
411                        $this->modified->timestamp() >= $entry['ts']) {
412                        break;
413                    }
414                    $this->modified = new Horde_Date($entry['ts']);
415                    if ($userId != $entry['who']) {
416                        $this->modifiedby = sprintf(_("by %s"), Kronolith::getUserName($entry['who']));
417                    } else {
418                        $this->modifiedby = _("by me");
419                    }
420                    break;
421                }
422            }
423        } catch (Horde_Exception $e) {
424        }
425    }
426
427    /**
428     * Setter.
429     *
430     * Sets the 'id' and 'creator' properties.
431     *
432     * @param string $name  Property name.
433     * @param mixed $value  Property value.
434     */
435    public function __set($name, $value)
436    {
437        switch ($name) {
438        case 'id':
439            if (substr($value, 0, 10) == 'kronolith:') {
440                $value = substr($value, 10);
441            }
442            // Fall through.
443        case 'creator':
444        case 'geoLocation':
445        case 'indent':
446        case 'originalStart':
447        case 'originalEnd':
448        case 'overlap':
449        case 'rowspan':
450        case 'span':
451        case 'tags':
452            $this->{'_' . $name} = $value;
453            return;
454        }
455        $trace = debug_backtrace();
456        trigger_error('Undefined property via __set(): ' . $name
457                      . ' in ' . $trace[0]['file']
458                      . ' on line ' . $trace[0]['line'],
459                      E_USER_NOTICE);
460    }
461
462    /**
463     * Getter.
464     *
465     * Returns the 'id' and 'creator' properties.
466     *
467     * @param string $name  Property name.
468     *
469     * @return mixed  Property value.
470     */
471    public function __get($name)
472    {
473        switch ($name) {
474        case 'id':
475        case 'indent':
476        case 'overlap':
477        case 'rowspan':
478        case 'span':
479            return $this->{'_' . $name};
480        case 'creator':
481            if (empty($this->_creator)) {
482                $this->_creator = $GLOBALS['registry']->getAuth();
483            }
484            return $this->_creator;
485            break;
486        case 'originalStart':
487            if (empty($this->_originalStart)) {
488                $this->_originalStart = $this->start;
489            }
490            return $this->_originalStart;
491            break;
492        case 'originalEnd':
493            if (empty($this->_originalEnd)) {
494                $this->_originalEnd = $this->start;
495            }
496            return $this->_originalEnd;
497            break;
498        case 'tags':
499            if (!isset($this->_tags)) {
500                $this->synchronizeTags(Kronolith::getTagger()->getTags($this->uid, Kronolith_Tagger::TYPE_EVENT));
501            }
502            return $this->_tags;
503        case 'geoLocation':
504            if (!isset($this->_geoLocation)) {
505                try {
506                    $this->_geoLocation = $GLOBALS['injector']->getInstance('Kronolith_Geo')->getLocation($this->id);
507                } catch (Kronolith_Exception $e) {}
508            }
509            return $this->_geoLocation;
510        }
511
512        $trace = debug_backtrace();
513        trigger_error('Undefined property via __set(): ' . $name
514                      . ' in ' . $trace[0]['file']
515                      . ' on line ' . $trace[0]['line'],
516                      E_USER_NOTICE);
517        return null;
518    }
519
520    /**
521     * Returns a reference to a driver that's valid for this event.
522     *
523     * @return Kronolith_Driver  A driver that this event can use to save
524     *                           itself, etc.
525     */
526    public function getDriver()
527    {
528        return Kronolith::getDriver(str_replace('Kronolith_Event_', '', get_class($this)), $this->calendar);
529    }
530
531    /**
532     * Returns the share this event belongs to.
533     *
534     * @return Horde_Share  This event's share.
535     * @throws Kronolith_Exception
536     */
537    public function getShare()
538    {
539        if ($GLOBALS['calendar_manager']->getEntry(Kronolith::ALL_CALENDARS, $this->calendar) !== false) {
540            return $GLOBALS['calendar_manager']->getEntry(Kronolith::ALL_CALENDARS, $this->calendar)->share();
541        }
542        throw new LogicException('Share not found');
543    }
544
545    /**
546     * Encapsulates permissions checking.
547     *
548     * @param integer $permission  The permission to check for.
549     * @param string $user         The user to check permissions for.
550     *
551     * @return boolean
552     */
553    public function hasPermission($permission, $user = null)
554    {
555        if ($user === null) {
556            $user = $GLOBALS['registry']->getAuth();
557        }
558        try {
559            $share = $this->getShare();
560        } catch (Exception $e) {
561            return false;
562        }
563        return $share->hasPermission($user, $permission, $this->creator);
564    }
565
566    /**
567     * Saves changes to this event.
568     *
569     * @return integer  The event id.
570     * @throws Kronolith_Exception
571     */
572    public function save()
573    {
574        if (!$this->initialized) {
575            throw new LogicException('Event not yet initialized');
576        }
577
578        /* Check for acceptance/denial of this event's resources. */
579        $accepted_resources = array();
580        $locks = $GLOBALS['injector']->getInstance('Horde_Lock');
581        $lock = array();
582        // Don't waste time with resource acceptance if the status is cancelled,
583        // the event will be removed from the resource calendar anyway.
584        if ($this->status != Kronolith::STATUS_CANCELLED) {
585            foreach (array_keys($this->getResources()) as $id) {
586                /* Get the resource and protect against infinite recursion in
587                 * case someone is silly enough to add a resource to it's own
588                 * event.*/
589                $resource = Kronolith::getDriver('Resource')->getResource($id);
590                $rcal = $resource->get('calendar');
591                if ($rcal == $this->calendar) {
592                    continue;
593                }
594                Kronolith::getDriver('Resource')->open($rcal);
595
596                /* Lock the resource and get the response */
597                if ($resource->get('response_type') == Kronolith_Resource::RESPONSETYPE_AUTO) {
598                    $principle = 'calendar/' . $rcal;
599                    $lock[$resource->getId()] = $locks->setLock($GLOBALS['registry']->getAuth(), 'kronolith', $principle, 5, Horde_Lock::TYPE_EXCLUSIVE);
600                    $haveLock = true;
601                } else {
602                    $haveLock = false;
603                }
604                if ($haveLock && !$lock[$resource->getId()]) {
605                    // Already locked
606                    // For now, just fail. Not sure how else to capture the
607                    // locked resources and notify the user.
608                    throw new Kronolith_Exception(sprintf(_("The resource \"%s\" was locked. Please try again."), $resource->get('name')));
609                } else {
610                    $response = $resource->getResponse($this);
611                }
612
613                /* Remember accepted resources so we can add the event to their
614                 * calendars. Otherwise, clear the lock. */
615                if ($response == Kronolith::RESPONSE_ACCEPTED) {
616                    $accepted_resources[] = $resource;
617                } elseif ($haveLock) {
618                    $locks->clearLock($lock[$resource->getId()]);
619                }
620
621                if ($response == Kronolith::RESPONSE_DECLINED && $this->uid) {
622                    $r_driver = Kronolith::getDriver('Resource');
623                    $r_event = $r_driver->getByUID($this->uid, array($resource->get('calendar')));
624                    $r_driver->deleteEvent($r_event, true, true);
625                }
626
627                /* Add the resource to the event */
628                $this->addResource($resource, $response);
629            }
630        } else {
631            // If event is cancelled, and actually exists, we need to mark it
632            // as cancelled in resource calendar.
633            foreach (array_keys($this->getResources()) as $id) {
634                $resource = Kronolith::getDriver('Resource')->getResource($id);
635                $rcal = $resource->get('calendar');
636                if ($rcal == $this->calendar) {
637                    continue;
638                }
639                try {
640                    Kronolith::getDriver('Resource')->open($rcal);
641                    $resource->addEvent($this);
642                } catch (Exception $e) {
643                }
644            }
645        }
646
647        /* Save */
648        $result = $this->getDriver()->saveEvent($this);
649
650        /* Now that the event is definitely commited to storage, we can add
651         * the event to each resource that has accepted. Not very efficient,
652         * but this also solves the problem of not having a GUID for the event
653         * until after it's saved. If we add the event to the resources
654         * calendar before it is saved, they will have different GUIDs, and
655         * hence no longer refer to the same event. */
656        foreach ($accepted_resources as $resource) {
657            $resource->addEvent($this);
658            if ($resource->get('response_type') == Kronolith_Resource::RESPONSETYPE_AUTO) {
659                $locks->clearLock($lock[$resource->getId()]);
660            }
661        }
662
663        $hordeAlarm = $GLOBALS['injector']->getInstance('Horde_Alarm');
664        if ($alarm = $this->toAlarm(new Horde_Date($_SERVER['REQUEST_TIME']))) {
665            $hordeAlarm->set($alarm);
666            if ($this->_snooze) {
667                $hordeAlarm->snooze($this->uid, $GLOBALS['registry']->getAuth(), $this->_snooze);
668            }
669        } else {
670            $hordeAlarm->delete($this->uid);
671        }
672
673        return $result;
674    }
675
676    /**
677     * Imports a backend specific event object.
678     *
679     * @param mixed $eventObject  Backend specific event object that this
680     *                            object will represent.
681     */
682    public function fromDriver($event)
683    {
684    }
685
686    /**
687     * Exports this event in iCalendar format.
688     *
689     * @param Horde_Icalendar $calendar  A Horde_Icalendar object that acts as
690     *                                   a container.
691     *
692     * @return array  An array of Horde_Icalendar_Vevent objects for this event.
693     */
694    public function toiCalendar($calendar)
695    {
696        $vEvent = Horde_Icalendar::newComponent('vevent', $calendar);
697        $v1 = $calendar->getAttribute('VERSION') == '1.0';
698        $vEvents = array();
699
700        // For certain recur types, we must output in the event's timezone
701        // so that the BYDAY values do not get out of sync with the UTC
702        // date-time. See Bug: 11339
703        if ($this->recurs()) {
704            switch ($this->recurrence->getRecurType()) {
705            case Horde_Date_Recurrence::RECUR_WEEKLY:
706            case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY:
707            case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY:
708                if (!$this->timezone) {
709                    $this->timezone = date_default_timezone_get();
710                }
711            }
712        }
713
714        if ($this->isAllDay()) {
715            $vEvent->setAttribute('DTSTART', $this->start, array('VALUE' => 'DATE'));
716            $vEvent->setAttribute('DTEND', $this->end, array('VALUE' => 'DATE'));
717            $vEvent->setAttribute('X-FUNAMBOL-ALLDAY', 1);
718        } else {
719            $this->setTimezone(true);
720            $params = array();
721            if ($this->timezone) {
722                try {
723                    if (!$this->baseid) {
724                        $tz = $GLOBALS['injector']->getInstance('Horde_Timezone');
725                        $vEvents[] = $tz->getZone($this->timezone)->toVtimezone();
726                    }
727                    $params['TZID'] = $this->timezone;
728                } catch (Horde_Exception $e) {
729                    Horde::log('Unable to locate the tz database.', 'WARN');
730                }
731            }
732
733            $vEvent->setAttribute('DTSTART', clone $this->start, $params);
734            $vEvent->setAttribute('DTEND', clone $this->end, $params);
735        }
736
737        $vEvent->setAttribute('DTSTAMP', $_SERVER['REQUEST_TIME']);
738        $vEvent->setAttribute('UID', $this->uid);
739
740        /* Get the event's create and last modify date. */
741        $created = $modified = null;
742        try {
743            $history = $GLOBALS['injector']->getInstance('Horde_History');
744            $created = $history->getActionTimestamp(
745                'kronolith:' . $this->calendar . ':' . $this->uid, 'add');
746            $modified = $history->getActionTimestamp(
747                'kronolith:' . $this->calendar . ':' . $this->uid, 'modify');
748            /* The history driver returns 0 for not found. If 0 or null does
749             * not matter, strip this. */
750            if ($created == 0) {
751                $created = null;
752            }
753            if ($modified == 0) {
754                $modified = null;
755            }
756        } catch (Exception $e) {
757        }
758        if (!empty($created)) {
759            $vEvent->setAttribute($v1 ? 'DCREATED' : 'CREATED', $created);
760            if (empty($modified)) {
761                $modified = $created;
762            }
763        }
764        if (!empty($modified)) {
765            $vEvent->setAttribute('LAST-MODIFIED', $modified);
766        }
767
768        $vEvent->setAttribute('SUMMARY', $this->getTitle());
769
770        // Organizer
771        if (count($this->attendees)) {
772            $name = Kronolith::getUserName($this->creator);
773            $email = Kronolith::getUserEmail($this->creator);
774            $params = array();
775            if ($v1) {
776                $tmp = new Horde_Mail_Rfc822_Address($email);
777                if (!empty($name)) {
778                    $tmp->personal = $name;
779                }
780                $email = strval($tmp);
781            } else {
782                if (!empty($name)) {
783                    $params['CN'] = $name;
784                }
785                if (!empty($email)) {
786                    $email = 'mailto:' . $email;
787                }
788            }
789            $vEvent->setAttribute('ORGANIZER', $email, $params);
790        }
791        if (!$this->isPrivate()) {
792            if (!empty($this->description)) {
793                $vEvent->setAttribute('DESCRIPTION', $this->description);
794            }
795
796            // Tags
797            if ($this->tags) {
798                $vEvent->setAttribute('CATEGORIES', '', array(), true, array_values($this->tags));
799            }
800
801            // Location
802            if (!empty($this->location)) {
803                $vEvent->setAttribute('LOCATION', $this->location);
804            }
805            if ($this->geoLocation) {
806                $vEvent->setAttribute('GEO', array('latitude' => $this->geoLocation['lat'], 'longitude' => $this->geoLocation['lon']));
807            }
808
809            // URL
810            if (!empty($this->url)) {
811                $vEvent->setAttribute('URL', $this->url);
812            }
813        }
814        $vEvent->setAttribute('CLASS', $this->private ? 'PRIVATE' : 'PUBLIC');
815
816        // Status.
817        switch ($this->status) {
818        case Kronolith::STATUS_FREE:
819            // This is not an official iCalendar value, but we need it for
820            // synchronization.
821            $vEvent->setAttribute('STATUS', 'FREE');
822            $vEvent->setAttribute('TRANSP', $v1 ? 1 : 'TRANSPARENT');
823            break;
824        case Kronolith::STATUS_TENTATIVE:
825            $vEvent->setAttribute('STATUS', 'TENTATIVE');
826            $vEvent->setAttribute('TRANSP', $v1 ? 0 : 'OPAQUE');
827            break;
828        case Kronolith::STATUS_CONFIRMED:
829            $vEvent->setAttribute('STATUS', 'CONFIRMED');
830            $vEvent->setAttribute('TRANSP', $v1 ? 0 : 'OPAQUE');
831            break;
832        case Kronolith::STATUS_CANCELLED:
833            if ($v1) {
834                $vEvent->setAttribute('STATUS', 'DECLINED');
835                $vEvent->setAttribute('TRANSP', 1);
836            } else {
837                $vEvent->setAttribute('STATUS', 'CANCELLED');
838                $vEvent->setAttribute('TRANSP', 'TRANSPARENT');
839            }
840            break;
841        }
842
843        // Attendees.
844        foreach ($this->attendees as $email => $status) {
845            $params = array();
846            switch ($status['attendance']) {
847            case Kronolith::PART_REQUIRED:
848                if ($v1) {
849                    $params['EXPECT'] = 'REQUIRE';
850                } else {
851                    $params['ROLE'] = 'REQ-PARTICIPANT';
852                }
853                break;
854
855            case Kronolith::PART_OPTIONAL:
856                if ($v1) {
857                    $params['EXPECT'] = 'REQUEST';
858                } else {
859                    $params['ROLE'] = 'OPT-PARTICIPANT';
860                }
861                break;
862
863            case Kronolith::PART_NONE:
864                if ($v1) {
865                    $params['EXPECT'] = 'FYI';
866                } else {
867                    $params['ROLE'] = 'NON-PARTICIPANT';
868                }
869                break;
870            }
871
872            switch ($status['response']) {
873            case Kronolith::RESPONSE_NONE:
874                if ($v1) {
875                    $params['STATUS'] = 'NEEDS ACTION';
876                    $params['RSVP'] = 'YES';
877                } else {
878                    $params['PARTSTAT'] = 'NEEDS-ACTION';
879                    $params['RSVP'] = 'TRUE';
880                }
881                break;
882
883            case Kronolith::RESPONSE_ACCEPTED:
884                if ($v1) {
885                    $params['STATUS'] = 'ACCEPTED';
886                } else {
887                    $params['PARTSTAT'] = 'ACCEPTED';
888                }
889                break;
890
891            case Kronolith::RESPONSE_DECLINED:
892                if ($v1) {
893                    $params['STATUS'] = 'DECLINED';
894                } else {
895                    $params['PARTSTAT'] = 'DECLINED';
896                }
897                break;
898
899            case Kronolith::RESPONSE_TENTATIVE:
900                if ($v1) {
901                    $params['STATUS'] = 'TENTATIVE';
902                } else {
903                    $params['PARTSTAT'] = 'TENTATIVE';
904                }
905                break;
906            }
907
908            if (strpos($email, '@') === false) {
909                $email = '';
910            }
911            if ($v1) {
912                if (empty($email)) {
913                    if (!empty($status['name'])) {
914                        $email = $status['name'];
915                    }
916                } else {
917                    $tmp = new Horde_Mail_Rfc822_Address($email);
918                    if (!empty($status['name'])) {
919                        $tmp->personal = $status['name'];
920                    }
921                    $email = strval($tmp);
922                }
923            } else {
924                if (!empty($status['name'])) {
925                    $params['CN'] = $status['name'];
926                }
927                if (!empty($email)) {
928                    $email = 'mailto:' . $email;
929                }
930            }
931
932            $vEvent->setAttribute('ATTENDEE', $email, $params);
933        }
934
935        // Alarms.
936        if (!empty($this->alarm)) {
937            if ($v1) {
938                $alarm = new Horde_Date($this->start);
939                $alarm->min -= $this->alarm;
940                $vEvent->setAttribute('AALARM', $alarm);
941            } else {
942                $vAlarm = Horde_Icalendar::newComponent('valarm', $vEvent);
943                $vAlarm->setAttribute('ACTION', 'DISPLAY');
944                $vAlarm->setAttribute('DESCRIPTION', $this->getTitle());
945                $vAlarm->setAttribute(
946                    'TRIGGER;VALUE=DURATION',
947                    ($this->alarm > 0 ? '-' : '') . 'PT' . abs($this->alarm) . 'M'
948                );
949                $vEvent->addComponent($vAlarm);
950            }
951            $hordeAlarm = $GLOBALS['injector']->getInstance('Horde_Alarm');
952            if ($hordeAlarm->exists($this->uid, $GLOBALS['registry']->getAuth()) &&
953                $hordeAlarm->isSnoozed($this->uid, $GLOBALS['registry']->getAuth())) {
954                $vEvent->setAttribute('X-MOZ-LASTACK', new Horde_Date($_SERVER['REQUEST_TIME']));
955                $alarm = $hordeAlarm->get($this->uid, $GLOBALS['registry']->getAuth());
956                if (!empty($alarm['snooze'])) {
957                    $alarm['snooze']->setTimezone(date_default_timezone_get());
958                    $vEvent->setAttribute('X-MOZ-SNOOZE-TIME', $alarm['snooze']);
959                }
960            }
961        }
962
963        // Recurrence.
964        if ($this->recurs()) {
965            if ($v1) {
966                $rrule = $this->recurrence->toRRule10($calendar);
967            } else {
968                $rrule = $this->recurrence->toRRule20($calendar);
969            }
970            if (!empty($rrule)) {
971                $vEvent->setAttribute('RRULE', $rrule);
972            }
973
974            // Exceptions. An exception with no replacement event is represented
975            // by EXDATE, and those with replacement events are represented by
976            // a new vEvent element. We get all known replacement events first,
977            // then remove the exceptionoriginaldate from the list of the event
978            // exceptions. Any exceptions left should represent exceptions with
979            // no replacement.
980            $exceptions = $this->recurrence->getExceptions();
981            $search = new stdClass();
982            $search->baseid = $this->uid;
983            $results = $this->getDriver()->search($search);
984            foreach ($results as $days) {
985                foreach ($days as $exceptionEvent) {
986                    // Need to change the UID so it links to the original
987                    // recurring event, but only if not using $v1. If using $v1,
988                    // we add the date to EXDATE and do NOT change the UID.
989                    if (!$v1) {
990                        $exceptionEvent->uid = $this->uid;
991                    }
992                    $vEventException = $exceptionEvent->toiCalendar($calendar);
993
994                    // This should never happen, but protect against it anyway.
995                    if (count($vEventException) > 2 ||
996                        (count($vEventException) > 1 &&
997                         !($vEventException[0] instanceof Horde_Icalendar_Vtimezone) &&
998                         !($vEventException[1] instanceof Horde_Icalendar_Vtimezone))) {
999                        throw new Kronolith_Exception(_("Unable to parse event."));
1000                    }
1001                    $vEventException = array_pop($vEventException);
1002                    // If $v1, need to add to EXDATE
1003                    if (!$this->isAllDay()) {
1004                        $exceptionEvent->setTimezone(true);
1005                    }
1006                    if (!$v1) {
1007                        $vEventException->setAttribute('RECURRENCE-ID', $exceptionEvent->exceptionoriginaldate);
1008                    } else {
1009                        $vEvent->setAttribute('EXDATE', array($exceptionEvent->exceptionoriginaldate), array('VALUE' => 'DATE'));
1010                    }
1011                    $originaldate = $exceptionEvent->exceptionoriginaldate->format('Ymd');
1012                    $key = array_search($originaldate, $exceptions);
1013                    if ($key !== false) {
1014                        unset($exceptions[$key]);
1015                    }
1016                    $vEvents[] = $vEventException;
1017                }
1018            }
1019
1020            /* The remaining exceptions represent deleted recurrences */
1021            foreach ($exceptions as $exception) {
1022                if (!empty($exception)) {
1023                    // Use multiple EXDATE attributes instead of EXDATE
1024                    // attributes with multiple values to make Apple iCal
1025                    // happy.
1026                    list($year, $month, $mday) = sscanf($exception, '%04d%02d%02d');
1027                    if ($this->isAllDay()) {
1028                        $vEvent->setAttribute('EXDATE', array(new Horde_Date($year, $month, $mday)), array('VALUE' => 'DATE'));
1029                    } else {
1030                        // Another Apple iCal/Calendar fix. EXDATE is only
1031                        // recognized if the full datetime is present and matches
1032                        // the time part given in DTSTART.
1033                        $params = array();
1034                        if ($this->timezone) {
1035                            $params['TZID'] = $this->timezone;
1036                        }
1037                        $exdate = clone $this->start;
1038                        $exdate->year = $year;
1039                        $exdate->month = $month;
1040                        $exdate->mday = $mday;
1041                        $vEvent->setAttribute('EXDATE', array($exdate), $params);
1042                    }
1043                }
1044            }
1045        }
1046        array_unshift($vEvents, $vEvent);
1047
1048        $this->setTimezone(false);
1049
1050        return $vEvents;
1051    }
1052
1053    /**
1054     * Updates the properties of this event from a Horde_Icalendar_Vevent
1055     * object.
1056     *
1057     * @param Horde_Icalendar_Vevent $vEvent  The iCalendar data to update
1058     *                                        from.
1059     * @param boolean $parseAttendees         Parse attendees too?
1060     *                                        @since Kronolith 4.2
1061     */
1062    public function fromiCalendar($vEvent, $parseAttendees = false)
1063    {
1064        // Unique ID.
1065        try {
1066            $uid = $vEvent->getAttribute('UID');
1067            if (!empty($uid)) {
1068                $this->uid = $uid;
1069            }
1070        } catch (Horde_Icalendar_Exception $e) {}
1071
1072        // Sequence.
1073        try {
1074            $seq = $vEvent->getAttribute('SEQUENCE');
1075            if (is_int($seq)) {
1076                $this->sequence = $seq;
1077            }
1078        } catch (Horde_Icalendar_Exception $e) {}
1079
1080        // Title, tags and description.
1081        try {
1082            $title = $this->_ensureUtf8($vEvent->getAttribute('SUMMARY'));
1083            if (!is_array($title)) {
1084                $this->title = $title;
1085            }
1086        } catch (Horde_Icalendar_Exception $e) {}
1087
1088        // Tags
1089        try {
1090            $this->_tags = $vEvent->getAttributeValues('CATEGORIES');
1091        } catch (Horde_Icalendar_Exception $e) {}
1092
1093        // Description
1094        try {
1095            $desc = $this->_ensureUtf8($vEvent->getAttribute('DESCRIPTION'));
1096            if (!is_array($desc)) {
1097                $this->description = $desc;
1098            }
1099        } catch (Horde_Icalendar_Exception $e) {}
1100
1101        // Remote Url
1102        try {
1103            $url = $vEvent->getAttribute('URL');
1104            if (!is_array($url)) {
1105                $this->url = $url;
1106            }
1107        } catch (Horde_Icalendar_Exception $e) {}
1108
1109        // Location
1110        try {
1111            $location = $this->_ensureUtf8($vEvent->getAttribute('LOCATION'));
1112            if (!is_array($location)) {
1113                $this->location = $location;
1114            }
1115        } catch (Horde_Icalendar_Exception $e) {}
1116
1117        try {
1118            $geolocation = $vEvent->getAttribute('GEO');
1119            $this->geoLocation = array(
1120                'lat' => $geolocation['latitude'],
1121                'lon' => $geolocation['longitude']
1122            );
1123        } catch (Horde_Icalendar_Exception $e) {}
1124
1125        // Class
1126        try {
1127            $class = $vEvent->getAttribute('CLASS');
1128            if (!is_array($class)) {
1129                $class = Horde_String::upper($class);
1130                $this->private = $class == 'PRIVATE' || $class == 'CONFIDENTIAL';
1131            }
1132        } catch (Horde_Icalendar_Exception $e) {}
1133
1134        // Status.
1135        try {
1136            $status = $vEvent->getAttribute('STATUS');
1137            if (!is_array($status)) {
1138                $status = Horde_String::upper($status);
1139                if ($status == 'DECLINED') {
1140                    $status = 'CANCELLED';
1141                }
1142                if (defined('Kronolith::STATUS_' . $status)) {
1143                    $this->status = constant('Kronolith::STATUS_' . $status);
1144                }
1145            }
1146        } catch (Horde_Icalendar_Exception $e) {}
1147
1148        // Reset allday flag in case this has changed. Will be recalculated
1149        // next time isAllDay() is called.
1150        $this->allday = false;
1151
1152        // Start and end date.
1153        $tzid = null;
1154        try {
1155            $start = $vEvent->getAttribute('DTSTART');
1156            $startParams = $vEvent->getAttribute('DTSTART', true);
1157            // We don't support different timezones for different attributes,
1158            // so use the DTSTART timezone for the complete event.
1159            if (isset($startParams[0]['TZID'])) {
1160                // Horde_Date supports timezone aliases, so try that first.
1161                $tz = $startParams[0]['TZID'];
1162                try {
1163                    // Check if the timezone name is supported by PHP natively.
1164                    new DateTimeZone($tz);
1165                    $this->timezone = $tzid = $tz;
1166                } catch (Exception $e) {
1167                }
1168            }
1169            if (!is_array($start)) {
1170                // Date-Time field
1171                $this->start = new Horde_Date($start, $tzid);
1172            } else {
1173                // Date field
1174                $this->start = new Horde_Date(
1175                    array('year'  => (int)$start['year'],
1176                          'month' => (int)$start['month'],
1177                          'mday'  => (int)$start['mday']),
1178                    $tzid
1179                );
1180            }
1181        } catch (Horde_Icalendar_Exception $e) {
1182            throw new Kronolith_Exception($e);
1183        } catch (Horde_Date_Exception $e) {
1184            throw new Kronolith_Exception($e);
1185        }
1186
1187        try {
1188            $end = $vEvent->getAttribute('DTEND');
1189            if (!is_array($end)) {
1190                // Date-Time field
1191                $this->end = new Horde_Date($end, $tzid);
1192                // All day events are transferred by many device as
1193                // DSTART: YYYYMMDDT000000 DTEND: YYYYMMDDT2359(59|00)
1194                // Convert accordingly
1195                if (is_object($this->start) && $this->start->hour == 0 &&
1196                    $this->start->min == 0 && $this->start->sec == 0 &&
1197                    $this->end->hour == 23 && $this->end->min == 59) {
1198                    $this->end = new Horde_Date(
1199                        array('year'  => (int)$this->end->year,
1200                              'month' => (int)$this->end->month,
1201                              'mday'  => (int)$this->end->mday + 1),
1202                        $tzid);
1203                }
1204            } else {
1205                // Date field
1206                $this->end = new Horde_Date(
1207                    array('year'  => (int)$end['year'],
1208                          'month' => (int)$end['month'],
1209                          'mday'  => (int)$end['mday']),
1210                    $tzid);
1211            }
1212        } catch (Horde_Icalendar_Exception $e) {
1213            $end = null;
1214        }
1215
1216        if (is_null($end)) {
1217            try {
1218                $duration = $vEvent->getAttribute('DURATION');
1219                if (!is_array($duration)) {
1220                    $this->end = new Horde_Date($this->start);
1221                    $this->end->sec += $duration;
1222                    $end = 1;
1223                }
1224            } catch (Horde_Icalendar_Exception $e) {}
1225
1226            if (is_null($end)) {
1227                // End date equal to start date as per RFC 2445.
1228                $this->end = new Horde_Date($this->start);
1229                if (is_array($start)) {
1230                    // Date field
1231                    $this->end->mday++;
1232                }
1233            }
1234        }
1235
1236        // vCalendar 1.0 alarms
1237        try {
1238            $alarm = $vEvent->getAttribute('AALARM');
1239            if (!is_array($alarm) && intval($alarm)) {
1240                $this->alarm = intval(($this->start->timestamp() - $alarm) / 60);
1241            }
1242        } catch (Horde_Icalendar_Exception $e) {}
1243
1244        // vCalendar 2.0 alarms
1245        foreach ($vEvent->getComponents() as $alarm) {
1246            if (!($alarm instanceof Horde_Icalendar_Valarm)) {
1247                continue;
1248            }
1249            try {
1250                if ($alarm->getAttribute('ACTION') == 'NONE') {
1251                    continue;
1252                }
1253            } catch (Horde_Icalendar_Exception $e) {
1254            }
1255            try {
1256                // @todo consider implementing different ACTION types.
1257                // $action = $alarm->getAttribute('ACTION');
1258                $trigger = $alarm->getAttribute('TRIGGER');
1259                $triggerParams = $alarm->getAttribute('TRIGGER', true);
1260            } catch (Horde_Icalendar_Exception $e) {
1261                continue;
1262            }
1263            if (!is_array($triggerParams)) {
1264                $triggerParams = array($triggerParams);
1265            }
1266            $haveTrigger = false;
1267            foreach ($triggerParams as $tp) {
1268                if (isset($tp['VALUE']) &&
1269                    $tp['VALUE'] == 'DATE-TIME') {
1270                    if (isset($tp['RELATED']) &&
1271                        $tp['RELATED'] == 'END') {
1272                        $this->alarm = intval(($this->end->timestamp() - $trigger) / 60);
1273                    } else {
1274                        $this->alarm = intval(($this->start->timestamp() - $trigger) / 60);
1275                    }
1276                    $haveTrigger = true;
1277                    break;
1278                } elseif (isset($tp['RELATED']) && $tp['RELATED'] == 'END') {
1279                    $this->alarm = -intval($trigger / 60);
1280                    $this->alarm -= $this->durMin;
1281                    $haveTrigger = true;
1282                    break;
1283                }
1284            }
1285            if (!$haveTrigger) {
1286                $this->alarm = -intval($trigger / 60);
1287            }
1288            break;
1289        }
1290
1291        // Alarm snoozing/dismissal
1292        if ($this->alarm) {
1293            try {
1294                // If X-MOZ-LASTACK is set, this event is either dismissed or
1295                // snoozed.
1296                $vEvent->getAttribute('X-MOZ-LASTACK');
1297                try {
1298                    // If X-MOZ-SNOOZE-TIME is set, this event is snoozed.
1299                    $snooze = $vEvent->getAttribute('X-MOZ-SNOOZE-TIME');
1300                    $this->_snooze = intval(($snooze - time()) / 60);
1301                } catch (Horde_Icalendar_Exception $e) {
1302                    // If X-MOZ-SNOOZE-TIME is not set, this event is dismissed.
1303                    $this->_snooze = -1;
1304                }
1305            } catch (Horde_Icalendar_Exception $e) {
1306            }
1307        }
1308
1309        // Attendance.
1310        // Importing attendance may result in confusion: editing an imported
1311        // copy of an event can cause invitation updates to be sent from
1312        // people other than the original organizer. So we don't import by
1313        // default. However to allow updates by synchronization, this behavior
1314        // can be overriden.
1315        // X-ATTENDEE is there for historical reasons. @todo remove in
1316        // Kronolith 5.
1317        $attendee = null;
1318        if ($parseAttendees) {
1319            try {
1320                $attendee = $vEvent->getAttribute('ATTENDEE');
1321                $params = $vEvent->getAttribute('ATTENDEE', true);
1322            } catch (Horde_Icalendar_Exception $e) {
1323                try {
1324                    $attendee = $vEvent->getAttribute('X-ATTENDEE');
1325                    $params = $vEvent->getAttribute('X-ATTENDEE', true);
1326                } catch (Horde_Icalendar_Exception $e) {
1327                }
1328            }
1329        }
1330        if ($attendee) {
1331            if (!is_array($attendee)) {
1332                $attendee = array($attendee);
1333            }
1334            if (!is_array($params)) {
1335                $params = array($params);
1336            }
1337            // Clear the attendees since we might be editing/replacing the event
1338            $this->attendees = array();
1339            for ($i = 0; $i < count($attendee); ++$i) {
1340                $attendee[$i] = str_replace(array('MAILTO:', 'mailto:'), '',
1341                                            $attendee[$i]);
1342                $tmp = new Horde_Mail_Rfc822_Address($attendee[$i]);
1343                $email = $tmp->bare_address;
1344                // Default according to rfc2445:
1345                $attendance = Kronolith::PART_REQUIRED;
1346                // vCalendar 2.0 style:
1347                if (!empty($params[$i]['ROLE'])) {
1348                    switch($params[$i]['ROLE']) {
1349                    case 'OPT-PARTICIPANT':
1350                        $attendance = Kronolith::PART_OPTIONAL;
1351                        break;
1352
1353                    case 'NON-PARTICIPANT':
1354                        $attendance = Kronolith::PART_NONE;
1355                        break;
1356                    }
1357                }
1358                // vCalendar 1.0 style;
1359                if (!empty($params[$i]['EXPECT'])) {
1360                    switch($params[$i]['EXPECT']) {
1361                    case 'REQUEST':
1362                        $attendance = Kronolith::PART_OPTIONAL;
1363                        break;
1364
1365                    case 'FYI':
1366                        $attendance = Kronolith::PART_NONE;
1367                        break;
1368                    }
1369                }
1370                $response = Kronolith::RESPONSE_NONE;
1371                if (empty($params[$i]['PARTSTAT']) &&
1372                    !empty($params[$i]['STATUS'])) {
1373                    $params[$i]['PARTSTAT']  = $params[$i]['STATUS'];
1374                }
1375
1376                if (!empty($params[$i]['PARTSTAT'])) {
1377                    switch($params[$i]['PARTSTAT']) {
1378                    case 'ACCEPTED':
1379                        $response = Kronolith::RESPONSE_ACCEPTED;
1380                        break;
1381
1382                    case 'DECLINED':
1383                        $response = Kronolith::RESPONSE_DECLINED;
1384                        break;
1385
1386                    case 'TENTATIVE':
1387                        $response = Kronolith::RESPONSE_TENTATIVE;
1388                        break;
1389                    }
1390                }
1391                $name = isset($params[$i]['CN'])
1392                    ? $this->_ensureUtf8($params[$i]['CN'])
1393                    : null;
1394
1395                $this->addAttendee($email, $attendance, $response, $name);
1396            }
1397        }
1398
1399        $this->_handlevEventRecurrence($vEvent);
1400
1401        $this->initialized = true;
1402    }
1403
1404    /**
1405     * Handle parsing recurrence related fields.
1406     *
1407     * @param Horde_Icalendar $vEvent
1408     * @throws Kronolith_Exception
1409     */
1410    protected function _handlevEventRecurrence($vEvent)
1411    {
1412        // Recurrence.
1413        try {
1414            $rrule = $vEvent->getAttribute('RRULE');
1415            if (!is_array($rrule)) {
1416                $this->recurrence = new Horde_Date_Recurrence($this->start);
1417                if (strpos($rrule, '=') !== false) {
1418                    $this->recurrence->fromRRule20($rrule);
1419                } else {
1420                    $this->recurrence->fromRRule10($rrule);
1421                }
1422
1423                // Exceptions. EXDATE represents deleted events, just add the
1424                // exception, no new event is needed.
1425                $exdates = $vEvent->getAttributeValues('EXDATE');
1426                if (is_array($exdates)) {
1427                    foreach ($exdates as $exdate) {
1428                        if (is_array($exdate)) {
1429                            $this->recurrence->addException(
1430                                (int)$exdate['year'],
1431                                (int)$exdate['month'],
1432                                (int)$exdate['mday']);
1433                        }
1434                    }
1435                }
1436            }
1437        } catch (Horde_Icalendar_Exception $e) {}
1438
1439        // RECURRENCE-ID indicates that this event represents an exception
1440        try {
1441            $recurrenceid = $vEvent->getAttribute('RECURRENCE-ID');
1442            $originaldt = new Horde_Date($recurrenceid);
1443            $this->exceptionoriginaldate = $originaldt;
1444            $this->baseid = $this->uid;
1445            $this->uid = null;
1446            try {
1447                $originalEvent = $this->getDriver()->getByUID($this->baseid);
1448                if ($originalEvent->recurrence) {
1449                    $originalEvent->recurrence->addException(
1450                        $originaldt->format('Y'),
1451                        $originaldt->format('m'),
1452                        $originaldt->format('d')
1453                    );
1454                    $originalEvent->save();
1455                }
1456            } catch (Horde_Exception_NotFound $e) {
1457                throw new Kronolith_Exception(_("Unable to locate original event series."));
1458            }
1459        } catch (Horde_Icalendar_Exception $e) {}
1460    }
1461
1462    /**
1463     * Imports the values for this event from a MS ActiveSync Message.
1464     *
1465     * @see Horde_ActiveSync_Message_Appointment
1466     */
1467    public function fromASAppointment(Horde_ActiveSync_Message_Appointment $message)
1468    {
1469        /* New event? */
1470        if ($this->id === null) {
1471            $this->creator = $GLOBALS['registry']->getAuth();
1472        }
1473        if (!$message->isGhosted('subject') &&
1474            strlen($title = $message->getSubject())) {
1475            $this->title = $title;
1476        }
1477        if ($message->getProtocolVersion() == Horde_ActiveSync::VERSION_TWOFIVE &&
1478            !$message->isGhosted('body') &&
1479            strlen($description = $message->getBody())) {
1480            $this->description = $description;
1481        } elseif ($message->getProtocolVersion() > Horde_ActiveSync::VERSION_TWOFIVE && !$message->isGhosted('airsyncbasebody')) {
1482            if ($message->airsyncbasebody->type == Horde_ActiveSync::BODYPREF_TYPE_HTML) {
1483                $this->description = Horde_Text_Filter::filter($message->airsyncbasebody->data, 'Html2text');
1484            } else {
1485                $this->description = $message->airsyncbasebody->data;
1486            }
1487        }
1488
1489        if (!$message->isGhosted('location') &&
1490            strlen($location = $message->getLocation())) {
1491            $this->location = $location;
1492        }
1493
1494        /* Date/times */
1495        $tz = !$message->isGhosted('timezone')
1496            ? $message->getTimezone()
1497            : $this->timezone;
1498        $dates = $message->getDatetime();
1499        $this->start = !$message->isGhosted('starttime')
1500            ? clone($dates['start'])
1501            : $this->start;
1502        $this->start->setTimezone($tz);
1503
1504        $this->end = !$message->isGhosted('endtime')
1505            ? clone($dates['end'])
1506            : $this->end;
1507        $this->end->setTimezone($tz);
1508
1509        if (!$message->isGhosted('alldayevent')) {
1510            $this->allday = $dates['allday'];
1511        }
1512        if ($tz != date_default_timezone_get()) {
1513            $this->timezone = $tz;
1514        }
1515
1516        /* Sensitivity */
1517        if (!$message->isGhosted('sensitivity')) {
1518            $this->private = ($message->getSensitivity() == Horde_ActiveSync_Message_Appointment::SENSITIVITY_PRIVATE || $message->getSensitivity() == Horde_ActiveSync_Message_Appointment::SENSITIVITY_CONFIDENTIAL) ? true :  false;
1519        }
1520
1521        /* Busy Status */
1522        if (!$message->isGhosted('meetingstatus')) {
1523            if ($message->getMeetingStatus() == Horde_ActiveSync_Message_Appointment::MEETING_CANCELLED) {
1524                $status = Kronolith::STATUS_CANCELLED;
1525            } else {
1526                $status = $message->getBusyStatus();
1527                switch ($status) {
1528                case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_BUSY:
1529                    $status = Kronolith::STATUS_CONFIRMED;
1530                    break;
1531
1532                case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_FREE:
1533                    $status = Kronolith::STATUS_FREE;
1534                    break;
1535
1536                case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_TENTATIVE:
1537                    $status = Kronolith::STATUS_TENTATIVE;
1538                    break;
1539                // @TODO: not sure how "Out" should show in kronolith...
1540                case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_OUT:
1541                    $status = Kronolith::STATUS_CONFIRMED;
1542                default:
1543                    // EAS Specifies default should be free.
1544                    $status = Kronolith::STATUS_FREE;
1545                }
1546            }
1547            $this->status = $status;
1548        }
1549
1550        /* Alarm */
1551        if (!$message->isGhosted('reminder') && ($alarm = $message->getReminder())) {
1552            $this->alarm = $alarm;
1553        }
1554
1555        /* Recurrence */
1556        if (!$message->isGhosted('recurrence') && ($rrule = $message->getRecurrence())) {
1557            /* Exceptions */
1558            $kronolith_driver = $this->getDriver();
1559            /* Since AS keeps exceptions as part of the original event, we need
1560             * to delete all existing exceptions and re-create them. The only
1561             * drawback to this is that the UIDs will change. */
1562            $this->recurrence = $rrule;
1563            if (!empty($this->uid)) {
1564                $search = new StdClass();
1565                $search->baseid = $this->uid;
1566                $results = $kronolith_driver->search($search);
1567                foreach ($results as $days) {
1568                    foreach ($days as $exception) {
1569                        $kronolith_driver->deleteEvent($exception->id);
1570                    }
1571                }
1572            }
1573
1574            $erules = $message->getExceptions();
1575            foreach ($erules as $rule){
1576                /* Add exception to recurrence obj*/
1577                $original = $rule->getExceptionStartTime();
1578                $original->setTimezone($tz);
1579                $this->recurrence->addException($original->format('Y'), $original->format('m'), $original->format('d'));
1580
1581                /* Readd the exception event, if not deleted */
1582                if (!$rule->deleted) {
1583                    $event = $kronolith_driver->getEvent();
1584                    $times = $rule->getDatetime();
1585                    $event->start = $times['start'];
1586                    $event->end = $times['end'];
1587                    $event->start->setTimezone($tz);
1588                    $event->end->setTimezone($tz);
1589                    $event->allday = $times['allday'];
1590                    $event->title = $rule->getSubject();
1591                    $event->title = empty($event->title) ? $this->title : $event->title;
1592                    $event->description = $rule->getBody();
1593                    $event->description = empty($event->description) ? $this->description : $event->description;
1594                    $event->baseid = $this->uid;
1595                    $event->exceptionoriginaldate = $original;
1596                    $event->initialized = true;
1597                    if ($tz != date_default_timezone_get()) {
1598                        $event->timezone = $tz;
1599                    }
1600                    $event->save();
1601                }
1602            }
1603        }
1604
1605        /* Attendees */
1606        if (!$message->isGhosted('attendees')) {
1607            $attendees = $message->getAttendees();
1608            foreach ($attendees as $attendee) {
1609                switch ($attendee->status) {
1610                case Horde_ActiveSync_Message_Attendee::STATUS_ACCEPT:
1611                    $response_code = Kronolith::RESPONSE_ACCEPTED;
1612                    break;
1613                case Horde_ActiveSync_Message_Attendee::STATUS_DECLINE:
1614                    $response_code = Kronolith::RESPONSE_DECLINED;
1615                    break;
1616                case Horde_ActiveSync_Message_Attendee::STATUS_TENTATIVE:
1617                    $response_code = Kronolith::RESPONSE_TENTATIVE;
1618                    break;
1619                default:
1620                    $response_code = Kronolith::RESPONSE_NONE;
1621                }
1622                switch ($attendee->type) {
1623                case Horde_ActiveSync_Message_Attendee::TYPE_REQUIRED:
1624                    $part_type = Kronolith::PART_REQUIRED;
1625                    break;
1626                case Horde_ActiveSync_Message_Attendee::TYPE_OPTIONAL:
1627                    $part_type = Kronolith::PART_OPTIONAL;
1628                    break;
1629                case Horde_ActiveSync_Message_Attendee::TYPE_RESOURCE:
1630                    $part_type = Kronolith::PART_REQUIRED;
1631                }
1632
1633                $this->addAttendee($attendee->email,
1634                                   $part_type,
1635                                   $response_code,
1636                                   $attendee->name);
1637            }
1638        }
1639
1640        /* Categories (Tags) */
1641        if (!$message->isGhosted('categories')) {
1642            $this->_tags = $message->getCategories();
1643        }
1644
1645        // 14.1
1646        if ($message->getProtocolVersion() >= Horde_ActiveSync::VERSION_FOURTEENONE &&
1647            !$message->isGhosted('onlinemeetingexternallink')) {
1648            $this->url = $message->onlinemeetingexternallink;
1649        }
1650
1651        /* Flag that we are initialized */
1652        $this->initialized = true;
1653    }
1654
1655    /**
1656     * Export this event as a MS ActiveSync Message
1657     *
1658     * @param array $options  Options:
1659     *   - protocolversion: (float)  The EAS version to support
1660     *                      DEFAULT: 2.5
1661     *   - bodyprefs: (array)  A BODYPREFERENCE array.
1662     *                DEFAULT: none (No body prefs enforced).
1663     *   - truncation: (integer)  Truncate event body to this length
1664     *                 DEFAULT: none (No truncation).
1665     *
1666     * @return Horde_ActiveSync_Message_Appointment
1667     */
1668    public function toASAppointment(array $options = array())
1669    {
1670        global $prefs, $registry;
1671
1672        $message = new Horde_ActiveSync_Message_Appointment(
1673            array(
1674                'logger' => $GLOBALS['injector']->getInstance('Horde_Log_Logger'),
1675                'protocolversion' => $options['protocolversion']
1676            )
1677        );
1678
1679        if (!$this->isPrivate()) {
1680            // Handle body/truncation
1681            if (!empty($options['bodyprefs'])) {
1682                if (Horde_String::length($this->description) > 0) {
1683                    $bp = $options['bodyprefs'];
1684                    $note = new Horde_ActiveSync_Message_AirSyncBaseBody();
1685                    // No HTML supported. Always use plaintext.
1686                    $note->type = Horde_ActiveSync::BODYPREF_TYPE_PLAIN;
1687                    if (isset($bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize'])) {
1688                        $truncation = $bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize'];
1689                    } elseif (isset($bp[Horde_ActiveSync::BODYPREF_TYPE_HTML])) {
1690                        $truncation = $bp[Horde_ActiveSync::BODYPREF_TYPE_HTML]['truncationsize'];
1691                        $this->description = Horde_Text_Filter::filter($this->description, 'Text2html', array('parselevel' => Horde_Text_Filter_Text2html::MICRO));
1692                    } else {
1693                        $truncation = false;
1694                    }
1695                    if ($truncation && Horde_String::length($this->description) > $truncation) {
1696                        $note->data = Horde_String::substr($this->description, 0, $truncation);
1697                        $note->truncated = 1;
1698                    } else {
1699                        $note->data = $this->description;
1700                    }
1701                    $note->estimateddatasize = Horde_String::length($this->description);
1702                    $message->airsyncbasebody = $note;
1703                }
1704            } else {
1705                $message->setBody($this->description);
1706            }
1707            $message->setLocation($this->location);
1708        }
1709
1710        $message->setSubject($this->getTitle());
1711        $message->setDatetime(array(
1712            'start' => $this->start,
1713            'end' => $this->end,
1714            'allday' => $this->isAllDay())
1715        );
1716        $message->setTimezone($this->start);
1717
1718        // Organizer
1719        if (count($this->attendees)) {
1720            if ($this->creator == $registry->getAuth()) {
1721                $as_ident = $prefs->getValue('activesync_identity') == 'horde'
1722                    ? $prefs->getValue('default_identity')
1723                    : $prefs->getValue('activesync_identity');
1724
1725                $name = $GLOBALS['injector']
1726                    ->getInstance('Horde_Core_Factory_Identity')
1727                    ->create($this->creator)->getValue('fullname', $as_ident);
1728                $email = $GLOBALS['injector']
1729                    ->getInstance('Horde_Core_Factory_Identity')
1730                    ->create($this->creator)->getValue('from_addr', $as_ident);
1731            } else {
1732                $name = Kronolith::getUserName($this->creator);
1733                $email = Kronolith::getUserEmail($this->creator);
1734            }
1735            $message->setOrganizer(array(
1736                'name' => $name,
1737                'email' => $email)
1738            );
1739        }
1740
1741        // Privacy
1742        $message->setSensitivity($this->private ?
1743            Horde_ActiveSync_Message_Appointment::SENSITIVITY_PRIVATE :
1744            Horde_ActiveSync_Message_Appointment::SENSITIVITY_NORMAL);
1745
1746        // Busy Status
1747        switch ($this->status) {
1748        case Kronolith::STATUS_CANCELLED:
1749            $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_FREE;
1750            break;
1751        case Kronolith::STATUS_CONFIRMED:
1752            $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_BUSY;
1753            break;
1754        case Kronolith::STATUS_TENTATIVE:
1755            $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_TENTATIVE;
1756        case Kronolith::STATUS_FREE:
1757        case Kronolith::STATUS_NONE:
1758            $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_FREE;
1759        }
1760        $message->setBusyStatus($status);
1761
1762        // DTStamp
1763        $message->setDTStamp($_SERVER['REQUEST_TIME']);
1764
1765        // Recurrence
1766        if ($this->recurs()) {
1767            $message->setRecurrence($this->recurrence, $GLOBALS['prefs']->getValue('week_start_monday'));
1768
1769            /* Exceptions are tricky. Exceptions, even those that represent
1770             * deleted instances of a recurring event, must be added. To do this
1771             * we query the storage for all the events that represent exceptions
1772             * (those with the baseid == $this->uid) and then remove the
1773             * exceptionoriginaldate from the list of exceptions we know about.
1774             * Any dates left in this list when we are done, must represent
1775             * deleted instances of this recurring event.*/
1776            if (!empty($this->recurrence) && $exceptions = $this->recurrence->getExceptions()) {
1777                $results = $this->boundExceptions();
1778                foreach ($results as $exception) {
1779                    $e = new Horde_ActiveSync_Message_Exception(array(
1780                        'protocolversion' => $options['protocolversion']));
1781                    $e->setDateTime(array(
1782                        'start' => $exception->start,
1783                        'end' => $exception->end,
1784                        'allday' => $exception->isAllDay()));
1785
1786                    // The start time of the *original* recurring event
1787                    $e->setExceptionStartTime($exception->exceptionoriginaldate);
1788                    $originaldate = $exception->exceptionoriginaldate->format('Ymd');
1789                    $key = array_search($originaldate, $exceptions);
1790                    if ($key !== false) {
1791                        unset($exceptions[$key]);
1792                    }
1793
1794                    // Remaining properties that could be different
1795                    $e->setSubject($exception->getTitle());
1796                    if (!$exception->isPrivate()) {
1797                        $e->setLocation($exception->location);
1798                        $e->setBody($exception->description);
1799                    }
1800
1801                    $e->setSensitivity($exception->private ?
1802                        Horde_ActiveSync_Message_Appointment::SENSITIVITY_PRIVATE :
1803                        Horde_ActiveSync_Message_Appointment::SENSITIVITY_NORMAL);
1804                    $e->setReminder($exception->alarm);
1805                    $e->setDTStamp($_SERVER['REQUEST_TIME']);
1806
1807                    if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWELVEONE) {
1808                        switch ($exception->status) {
1809                        case Kronolith::STATUS_TENTATIVE;
1810                            $e->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_TENTATIVE;
1811                            break;
1812                        case Kronolith::STATUS_NONE:
1813                            $e->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_NORESPONSE;
1814                            break;
1815                        case Kronolith::STATUS_CONFIRMED:
1816                            $e->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_ACCEPTED;
1817                            break;
1818                        default:
1819                            $e->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_NONE;
1820                        }
1821                    }
1822
1823                    // Tags/Categories
1824                    if (!$exception->isPrivate()) {
1825                        foreach ($exception->tags as $tag) {
1826                            $e->addCategory($tag);
1827                        }
1828                    }
1829
1830                    $message->addexception($e);
1831                }
1832
1833                // Any dates left in $exceptions must be deleted exceptions
1834                foreach ($exceptions as $deleted) {
1835                    $e = new Horde_ActiveSync_Message_Exception(array(
1836                        'protocolversion' => $options['protocolversion']));
1837                    // Kronolith stores the date only, but some AS clients need
1838                    // the datetime.
1839                    list($year, $month, $mday) = sscanf($deleted, '%04d%02d%02d');
1840                    $st = clone $this->start;
1841                    $st->year = $year;
1842                    $st->month = $month;
1843                    $st->mday = $mday;
1844                    $e->setExceptionStartTime($st);
1845                    $e->deleted = true;
1846                    $message->addException($e);
1847                }
1848            }
1849        }
1850
1851        // Attendees
1852        if (!$this->isPrivate() && count($this->attendees)) {
1853            $message->setMeetingStatus(
1854                $this->status == Kronolith::STATUS_CANCELLED
1855                    ? Horde_ActiveSync_Message_Appointment::MEETING_CANCELLED
1856                    : Horde_ActiveSync_Message_Appointment::MEETING_IS_MEETING
1857            );
1858            foreach ($this->attendees as $email => $properties) {
1859                $attendee = new Horde_ActiveSync_Message_Attendee(array(
1860                    'protocolversion' => $options['protocolversion']));
1861                $adr_obj = new Horde_Mail_Rfc822_Address($email);
1862                $attendee->name = $adr_obj->label;
1863                $attendee->email = $adr_obj->bare_address;
1864
1865                // AS only has required or optional, and only EAS Version > 2.5
1866                if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWOFIVE) {
1867                    $attendee->type = ($properties['attendance'] !== Kronolith::PART_REQUIRED
1868                        ? Horde_ActiveSync_Message_Attendee::TYPE_OPTIONAL
1869                        : Horde_ActiveSync_Message_Attendee::TYPE_REQUIRED);
1870
1871                    switch ($properties['response']) {
1872                    case Kronolith::RESPONSE_NONE:
1873                        $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_NORESPONSE;
1874                        break;
1875                    case Kronolith::RESPONSE_ACCEPTED:
1876                        $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_ACCEPT;
1877                        break;
1878                    case Kronolith::RESPONSE_DECLINED:
1879                        $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_DECLINE;
1880                        break;
1881                    case Kronolith::RESPONSE_TENTATIVE:
1882                        $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_TENTATIVE;
1883                        break;
1884                    default:
1885                        $attendee->status = Horde_ActiveSync_Message_Attendee::STATUS_UNKNOWN;
1886                    }
1887                }
1888
1889                $message->addAttendee($attendee);
1890            }
1891        } elseif ($this->status == Kronolith::STATUS_CANCELLED) {
1892            $message->setMeetingStatus(Horde_ActiveSync_Message_Appointment::MEETING_CANCELLED);
1893        } else {
1894            $message->setMeetingStatus(Horde_ActiveSync_Message_Appointment::MEETING_NOT_MEETING);
1895        }
1896
1897        // Resources
1898        if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWOFIVE) {
1899            $r = $this->getResources();
1900            foreach ($r as $id => $data) {
1901                $resource = Kronolith::getDriver('Resource')->getResource($id);
1902                // EAS *REQUIRES* an email field for Resources. If it is missing
1903                // a number of clients will fail, losing push.
1904                if ($resource->get('email')) {
1905                    $attendee = new Horde_ActiveSync_Message_Attendee(array(
1906                        'protocolversion' => $options['protocolversion']));
1907                    $attendee->email = $resource->get('email');
1908                    $attendee->type = Horde_ActiveSync_Message_Attendee::TYPE_RESOURCE;
1909                    $attendee->name = $data['name'];
1910                    $attendee->status = $data['response'];
1911                    $message->addAttendee($attendee);
1912                }
1913           }
1914        }
1915
1916        // Reminder
1917        if ($this->alarm) {
1918            $message->setReminder($this->alarm);
1919        }
1920
1921        // Categories (tags)
1922        if (!$this->isPrivate()) {
1923            foreach ($this->tags as $tag) {
1924                $message->addCategory($tag);
1925            }
1926        }
1927
1928        // EAS 14
1929        if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWELVEONE) {
1930            // We don't track the actual responses we sent to other's invitations.
1931            // Set this based on the status flag.
1932            switch ($this->status) {
1933            case Kronolith::STATUS_TENTATIVE;
1934                $message->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_TENTATIVE;
1935                break;
1936            case Kronolith::STATUS_NONE:
1937                $message->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_NORESPONSE;
1938                break;
1939            case Kronolith::STATUS_CONFIRMED:
1940                $message->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_ACCEPTED;
1941                break;
1942            default:
1943                $message->responsetype = Horde_ActiveSync_Message_Appointment::RESPONSE_NONE;
1944            }
1945        }
1946
1947        // 14.1
1948        if ($options['protocolversion'] >= Horde_ActiveSync::VERSION_FOURTEENONE) {
1949            $message->onlinemeetingexternallink = $this->url;
1950        }
1951
1952        return $message;
1953    }
1954
1955    /**
1956     * Imports the values for this event from an array of values.
1957     *
1958     * @param array $hash  Array containing all the values.
1959     *
1960     * @throws Kronolith_Exception
1961     */
1962    public function fromHash($hash)
1963    {
1964        // See if it's a new event.
1965        if ($this->id === null) {
1966            $this->creator = $GLOBALS['registry']->getAuth();
1967        }
1968
1969        if (!empty($hash['title'])) {
1970            $this->title = $hash['title'];
1971        } else {
1972            throw new Kronolith_Exception(_("Events must have a title."));
1973        }
1974
1975        $this->start = null;
1976        if (!empty($hash['start_date'])) {
1977            $date = array_map('intval', explode('-', $hash['start_date']));
1978            if (empty($hash['start_time'])) {
1979                $time = array(0, 0, 0);
1980            } else {
1981                $time = array_map('intval', explode(':', $hash['start_time']));
1982                if (count($time) == 2) {
1983                    $time[2] = 0;
1984                }
1985            }
1986            if (count($time) == 3 && count($date) == 3 &&
1987                !empty($date[1]) && !empty($date[2])) {
1988                if ($date[0] < 100) {
1989                    $date[0] += (date('Y') / 100 | 0) * 100;
1990                }
1991                $this->start = new Horde_Date(
1992                    array(
1993                        'year'  => $date[0],
1994                        'month' => $date[1],
1995                        'mday'  => $date[2],
1996                        'hour'  => $time[0],
1997                        'min'   => $time[1],
1998                        'sec'   => $time[2]
1999                    ),
2000                    isset($hash['timezone']) ? $hash['timezone'] : null
2001                );
2002            }
2003        }
2004        if (!isset($this->start)) {
2005            throw new Kronolith_Exception(_("Events must have a start date."));
2006        }
2007
2008        if (empty($hash['duration'])) {
2009            if (empty($hash['end_date'])) {
2010                $hash['end_date'] = $hash['start_date'];
2011            }
2012            if (empty($hash['end_time'])) {
2013                $hash['end_time'] = $hash['start_time'];
2014            }
2015        } else {
2016            $weeks = str_replace('W', '', $hash['duration'][1]);
2017            $days = str_replace('D', '', $hash['duration'][2]);
2018            $hours = str_replace('H', '', $hash['duration'][4]);
2019            $minutes = isset($hash['duration'][5]) ? str_replace('M', '', $hash['duration'][5]) : 0;
2020            $seconds = isset($hash['duration'][6]) ? str_replace('S', '', $hash['duration'][6]) : 0;
2021            $hash['duration'] = ($weeks * 60 * 60 * 24 * 7) + ($days * 60 * 60 * 24) + ($hours * 60 * 60) + ($minutes * 60) + $seconds;
2022            $this->end = new Horde_Date($this->start);
2023            $this->end->sec += $hash['duration'];
2024        }
2025        if (!empty($hash['end_date'])) {
2026            $date = array_map('intval', explode('-', $hash['end_date']));
2027            if (empty($hash['end_time'])) {
2028                $time = array(0, 0, 0);
2029            } else {
2030                $time = array_map('intval', explode(':', $hash['end_time']));
2031                if (count($time) == 2) {
2032                    $time[2] = 0;
2033                }
2034            }
2035            if (count($time) == 3 && count($date) == 3 &&
2036                !empty($date[1]) && !empty($date[2])) {
2037                if ($date[0] < 100) {
2038                    $date[0] += (date('Y') / 100 | 0) * 100;
2039                }
2040                $this->end = new Horde_Date(
2041                    array(
2042                        'year'  => $date[0],
2043                        'month' => $date[1],
2044                        'mday'  => $date[2],
2045                        'hour'  => $time[0],
2046                        'min'   => $time[1],
2047                        'sec'   => $time[2]
2048                    ),
2049                    isset($hash['timezone']) ? $hash['timezone'] : null
2050                );
2051            }
2052        }
2053
2054        if (!empty($hash['alarm'])) {
2055            $this->alarm = (int)$hash['alarm'];
2056        } elseif (!empty($hash['alarm_date']) &&
2057                  !empty($hash['alarm_time'])) {
2058            $date = array_map('intval', explode('-', $hash['alarm_date']));
2059            $time = array_map('intval', explode(':', $hash['alarm_time']));
2060            if (count($time) == 2) {
2061                $time[2] = 0;
2062            }
2063            if (count($time) == 3 && count($date) == 3 &&
2064                !empty($date[1]) && !empty($date[2])) {
2065                $alarm = new Horde_Date(
2066                    array(
2067                        'year'  => $date[0],
2068                        'month' => $date[1],
2069                        'mday'  => $date[2],
2070                        'hour'  => $time[0],
2071                        'min'   => $time[1],
2072                        'sec'   => $time[2]
2073                    ),
2074                    isset($hash['timezone']) ? $hash['timezone'] : null
2075                );
2076                $this->alarm = ($this->start->timestamp() - $alarm->timestamp()) / 60;
2077            }
2078        }
2079
2080        $this->allday = !empty($hash['allday']);
2081
2082        if (!empty($hash['description'])) {
2083            $this->description = $hash['description'];
2084        }
2085
2086        if (!empty($hash['location'])) {
2087            $this->location = $hash['location'];
2088        }
2089
2090        // Import once we support organizers.
2091        /*
2092        if (!empty($hash['organizer'])) {
2093            $this->organizer = $hash['organizer'];
2094        }
2095        */
2096
2097        if (!empty($hash['private'])) {
2098            $this->private = true;
2099        }
2100
2101        if (!empty($hash['recur_type'])) {
2102            $this->recurrence = new Horde_Date_Recurrence($this->start);
2103            $this->recurrence->setRecurType($hash['recur_type']);
2104            if (!empty($hash['recur_count'])) {
2105                $this->recurrence->setRecurCount($hash['recur_count']);
2106            } elseif (!empty($hash['recur_end_date'])) {
2107                $date = array_map('intval', explode('-', $hash['recur_end_date']));
2108                if (count($date) == 3 && !empty($date[1]) && !empty($date[2])) {
2109                    $this->recurrence->setRecurEnd(
2110                        new Horde_Date(array(
2111                            'year'  => $date[0],
2112                            'month' => $date[1],
2113                            'mday'  => $date[2]
2114                        ))
2115                    );
2116                }
2117            }
2118            if (!empty($hash['recur_interval'])) {
2119                $this->recurrence->setRecurInterval($hash['recur_interval']);
2120            }
2121            if (!empty($hash['recur_data'])) {
2122                $this->recurrence->setRecurOnDay($hash['recur_data']);
2123            }
2124            if (!empty($hash['recur_exceptions'])) {
2125                foreach ($hash['recur_exceptions'] as $exception) {
2126                    $parts = explode('-', $exception);
2127                    if (count($parts) == 3) {
2128                        $this->recurrence->addException($parts[0], $parts[1], $parts[2]);
2129                    }
2130                }
2131            }
2132        }
2133
2134        if (isset($hash['sequence'])) {
2135            $this->sequence = $hash['sequence'];
2136        }
2137
2138        if (!empty($hash['tags'])) {
2139            $this->tags = $hash['tags'];
2140        }
2141
2142        if (!empty($hash['timezone'])) {
2143            $this->timezone = $hash['timezone'];
2144        }
2145
2146        if (!empty($hash['uid'])) {
2147            $this->uid = $hash['uid'];
2148        }
2149
2150        $this->initialized = true;
2151    }
2152
2153    /**
2154     * Returns an alarm hash of this event suitable for Horde_Alarm.
2155     *
2156     * @param Horde_Date $time  Time of alarm.
2157     * @param string $user      The user to return alarms for.
2158     * @param Prefs $prefs      A Prefs instance.
2159     *
2160     * @return array  Alarm hash or null.
2161     */
2162    public function toAlarm($time, $user = null, $prefs = null)
2163    {
2164        if (!$this->alarm || $this->status == Kronolith::STATUS_CANCELLED) {
2165            return;
2166        }
2167
2168        if ($this->recurs()) {
2169            $eventDate = $this->recurrence->nextRecurrence($time);
2170            if (!$eventDate || ($eventDate && $this->recurrence->hasException($eventDate->year, $eventDate->month, $eventDate->mday))) {
2171                return;
2172            }
2173            $start = clone $eventDate;
2174            $diff = Date_Calc::dateDiff(
2175                $this->start->mday,
2176                $this->start->month,
2177                $this->start->year,
2178                $this->end->mday,
2179                $this->end->month,
2180                $this->end->year
2181            );
2182            if ($diff == -1) {
2183                $diff = 0;
2184            }
2185            $end = new Horde_Date(array(
2186                'year' => $start->year,
2187                'month' => $start->month,
2188                'mday' => $start->mday + $diff,
2189                'hour' => $this->end->hour,
2190                'min' => $this->end->min,
2191                'sec' => $this->end->sec)
2192            );
2193        } else {
2194            $start = clone $this->start;
2195            $end = clone $this->end;
2196        }
2197
2198        $serverName = $_SERVER['SERVER_NAME'];
2199        $serverConf = $GLOBALS['conf']['server']['name'];
2200        if (!empty($GLOBALS['conf']['reminder']['server_name'])) {
2201            $_SERVER['SERVER_NAME'] = $GLOBALS['conf']['server']['name'] = $GLOBALS['conf']['reminder']['server_name'];
2202        }
2203
2204        if (empty($user)) {
2205            $user = $GLOBALS['registry']->getAuth();
2206        }
2207        if (empty($prefs)) {
2208            $prefs = $GLOBALS['prefs'];
2209        }
2210
2211        $methods = !empty($this->methods) ? $this->methods : @unserialize($prefs->getValue('event_alarms'));
2212        if (isset($methods['notify'])) {
2213            $methods['notify']['show'] = array(
2214                '__app' => $GLOBALS['registry']->getApp(),
2215                'event' => $this->id,
2216                'calendar' => $this->calendar);
2217            $methods['notify']['ajax'] = 'event:' . $this->calendarType . '|' . $this->calendar . ':' . $this->id . ':' . $start->dateString();
2218            if (!empty($methods['notify']['sound'])) {
2219                if ($methods['notify']['sound'] == 'on') {
2220                    // Handle boolean sound preferences.
2221                    $methods['notify']['sound'] = (string)Horde_Themes::sound('theetone.wav');
2222                } else {
2223                    // Else we know we have a sound name that can be
2224                    // served from Horde.
2225                    $methods['notify']['sound'] = (string)Horde_Themes::sound($methods['notify']['sound']);
2226                }
2227            }
2228            if ($this->isAllDay()) {
2229                if ($start->compareDate($end) == 0) {
2230                    $methods['notify']['subtitle'] = sprintf(_("On %s"), '<strong>' . $start->strftime($prefs->getValue('date_format')) . '</strong>');
2231                } else {
2232                    $methods['notify']['subtitle'] = sprintf(_("From %s to %s"), '<strong>' . $start->strftime($prefs->getValue('date_format')) . '</strong>', '<strong>' . $end->strftime($prefs->getValue('date_format')) . '</strong>');
2233                }
2234            } else {
2235                $methods['notify']['subtitle'] = sprintf(_("From %s at %s to %s at %s"), '<strong>' . $start->strftime($prefs->getValue('date_format')), $start->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia') . '</strong>', '<strong>' . $end->strftime($prefs->getValue('date_format')), $this->end->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia') . '</strong>');
2236            }
2237        }
2238        if (isset($methods['mail'])) {
2239            $image = Kronolith::getImagePart('big_alarm.png');
2240
2241            $view = new Horde_View(array('templatePath' => KRONOLITH_TEMPLATES . '/alarm', 'encoding' => 'UTF-8'));
2242            new Horde_View_Helper_Text($view);
2243            $view->event = $this;
2244            $view->imageId = $image->getContentId();
2245            $view->user = $user;
2246            $view->dateFormat = $prefs->getValue('date_format');
2247            $view->timeFormat = $prefs->getValue('twentyFour') ? 'H:i' : 'h:ia';
2248            $view->start = $start;
2249            if (!$prefs->isLocked('event_reminder')) {
2250                $view->prefsUrl = Horde::url($GLOBALS['registry']->getServiceLink('prefs', 'kronolith'), true)->remove(session_name());
2251            }
2252            if ($this->attendees) {
2253                $view->attendees = Kronolith::getAttendeeEmailList($this->attendees)->addresses;
2254            }
2255
2256            $methods['mail']['mimepart'] = Kronolith::buildMimeMessage($view, 'mail', $image);
2257        }
2258        if (isset($methods['desktop'])) {
2259            if ($this->isAllDay()) {
2260                if ($this->start->compareDate($this->end) == 0) {
2261                    $methods['desktop']['subtitle'] = sprintf(_("On %s"), $start->strftime($prefs->getValue('date_format')));
2262                } else {
2263                    $methods['desktop']['subtitle'] = sprintf(_("From %s to %s"), $start->strftime($prefs->getValue('date_format')), $end->strftime($prefs->getValue('date_format')));
2264                }
2265            } else {
2266                $methods['desktop']['subtitle'] = sprintf(_("From %s at %s to %s at %s"), $start->strftime($prefs->getValue('date_format')), $start->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia'), $end->strftime($prefs->getValue('date_format')), $this->end->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia'));
2267            }
2268            $methods['desktop']['url'] = strval($this->getViewUrl(array(), true, false));
2269        }
2270
2271        $alarmStart = clone $start;
2272        $alarmStart->min -= $this->alarm;
2273        $alarm = array(
2274            'id' => $this->uid,
2275            'user' => $user,
2276            'start' => $alarmStart,
2277            'end' => $end,
2278            'methods' => array_keys($methods),
2279            'params' => $methods,
2280            'title' => $this->getTitle($user),
2281            'text' => $this->description,
2282            'instanceid' => $this->recurs() ? $eventDate->dateString() : null);
2283
2284        $_SERVER['SERVER_NAME'] = $serverName;
2285        $GLOBALS['conf']['server']['name'] = $serverConf;
2286
2287        return $alarm;
2288    }
2289
2290    /**
2291     * Returns a simple object suitable for json transport representing this
2292     * event.
2293     *
2294     * Possible properties are:
2295     * - t: title
2296     * - d: description
2297     * - c: calendar id
2298     * - s: start date
2299     * - e: end date
2300     * - fi: first day of a multi-day event
2301     * - la: last day of a multi-day event
2302     * - x: status (Kronolith::STATUS_* constant)
2303     * - al: all-day?
2304     * - bg: background color
2305     * - fg: foreground color
2306     * - pe: edit permissions?
2307     * - pd: delete permissions?
2308     * - vl: variable, i.e. editable length?
2309     * - a: alarm text or minutes
2310     * - r: recurrence type (Horde_Date_Recurrence::RECUR_* constant)
2311     * - bid: The baseid for an event representing an exception
2312     * - eod: The original date that an exception is replacing
2313     * - ic: icon
2314     * - ln: link
2315     * - aj: ajax link
2316     * - id: event id
2317     * - ty: calendar type (driver)
2318     * - l: location
2319     * - u: url
2320     * - sd: formatted start date
2321     * - st: formatted start time
2322     * - ed: formatted end date
2323     * - et: formatted end time
2324     * - at: attendees
2325     * - rs:  resources
2326     * - tg: tag list,
2327     * - mt: meeting (Boolean true if event has attendees, false otherwise).
2328     *
2329     * @param boolean $allDay      If not null, overrides whether the event is
2330     *                             an all-day event.
2331     * @param boolean $full        Whether to return all event details.
2332     * @param string $time_format  The date() format to use for time formatting.
2333     *
2334     * @return stdClass  A simple object.
2335     */
2336    public function toJson($allDay = null, $full = false, $time_format = 'H:i')
2337    {
2338        $json = new stdClass;
2339        $json->uid = $this->uid;
2340        $json->t = $this->getTitle();
2341        $json->c = $this->calendar;
2342        $json->s = $this->start->toJson();
2343        $json->e = $this->end->toJson();
2344        $json->fi = $this->first;
2345        $json->la = $this->last;
2346        $json->x = (int)$this->status;
2347        $json->al = is_null($allDay) ? $this->isAllDay() : $allDay;
2348        $json->pe = $this->hasPermission(Horde_Perms::EDIT);
2349        $json->pd = $this->hasPermission(Horde_Perms::DELETE);
2350        $json->l = $this->getLocation();
2351        $json->mt = !empty($this->attendees);
2352        $json->sort = sprintf(
2353            '%010s%06s',
2354            $this->originalStart->timestamp(),
2355            240000 - $this->end->format('His')
2356        );
2357
2358        if ($this->icon) {
2359            $json->ic = $this->icon;
2360        }
2361        if ($this->alarm) {
2362            if ($this->alarm % 10080 == 0) {
2363                $alarm_value = $this->alarm / 10080;
2364                $json->a = sprintf(ngettext("%d week", "%d weeks", $alarm_value), $alarm_value);
2365            } elseif ($this->alarm % 1440 == 0) {
2366                $alarm_value = $this->alarm / 1440;
2367                $json->a = sprintf(ngettext("%d day", "%d days", $alarm_value), $alarm_value);
2368            } elseif ($this->alarm % 60 == 0) {
2369                $alarm_value = $this->alarm / 60;
2370                $json->a = sprintf(ngettext("%d hour", "%d hours", $alarm_value), $alarm_value);
2371            } else {
2372                $alarm_value = $this->alarm;
2373                $json->a = sprintf(ngettext("%d minute", "%d minutes", $alarm_value), $alarm_value);
2374            }
2375        }
2376        if ($this->recurs()) {
2377            $json->r = $this->recurrence->getRecurType();
2378        } elseif ($this->baseid) {
2379            $json->bid = $this->baseid;
2380            if ($this->exceptionoriginaldate) {
2381                $json->eod = sprintf(_("%s at %s"), $this->exceptionoriginaldate->strftime($GLOBALS['prefs']->getValue('date_format')), $this->exceptionoriginaldate->strftime(($GLOBALS['prefs']->getValue('twentyFour') ? '%H:%M' : '%I:%M %p')));
2382            }
2383        }
2384        if ($this->_resources) {
2385            $json->rs = $this->_resources;
2386        }
2387        if ($full) {
2388            $json->id = $this->id;
2389            $json->ty = $this->calendarType;
2390            $json->sd = $this->start->strftime('%x');
2391            $json->st = $this->start->format($time_format);
2392            $json->ed = $this->end->strftime('%x');
2393            $json->et = $this->end->format($time_format);
2394            $json->tz = $this->timezone;
2395            $json->a = $this->alarm;
2396            $json->pv = $this->private;
2397            if ($this->recurs()) {
2398                $json->r = $this->recurrence->toJson();
2399            }
2400            if (!$this->isPrivate()) {
2401                $json->d = $this->description;
2402                $json->u =  htmlentities($this->url);
2403                $json->uhl = $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter(
2404                    $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($this->url, 'linkurls'),
2405                    'Xss'
2406                );
2407                $json->tg = array_values($this->tags);
2408                $json->gl = $this->geoLocation;
2409                if ($this->attendees) {
2410                    $attendees = array();
2411                    foreach ($this->attendees as $email => $info) {
2412                        $tmp = new Horde_Mail_Rfc822_Address($email);
2413                        if (!empty($info['name'])) {
2414                            $tmp->personal = $info['name'];
2415                        }
2416
2417                        $attendees[] = array(
2418                            'a' => intval($info['attendance']),
2419                            'e' => $tmp->bare_address,
2420                            'r' => intval($info['response']),
2421                            'l' => strval($tmp)
2422                        );
2423                        $json->at = $attendees;
2424                    }
2425                }
2426            }
2427            if ($this->methods) {
2428                $json->m = $this->methods;
2429            }
2430        }
2431
2432        return $json;
2433    }
2434
2435    /**
2436     * Checks if the current event is already present in the calendar.
2437     *
2438     * Does the check based on the uid.
2439     *
2440     * @return boolean  True if event exists, false otherwise.
2441     */
2442    public function exists()
2443    {
2444        if (!isset($this->uid) || !isset($this->calendar)) {
2445            return false;
2446        }
2447        try {
2448            $eventID = $this->getDriver()->exists($this->uid, $this->calendar);
2449            if (!$eventID) {
2450                return false;
2451            }
2452        } catch (Exception $e) {
2453            return false;
2454        }
2455        $this->id = $eventID;
2456        return true;
2457    }
2458
2459    /**
2460     * Converts this event between the event's and the local timezone.
2461     *
2462     * @param boolean $to_orginal  If true converts to the event's timezone.
2463     */
2464    public function setTimezone($to_original)
2465    {
2466        if (!$this->timezone || !$this->getDriver()->supportsTimezones()) {
2467            return;
2468        }
2469        $timezone = $to_original ? $this->timezone : date_default_timezone_get();
2470        $this->start->setTimezone($timezone);
2471        $this->end->setTimezone($timezone);
2472        if ($this->recurs() && $this->recurrence->hasRecurEnd()) {
2473            /* @todo Check if have to go through all recurrence
2474               exceptions too. */
2475            $this->recurrence->start->setTimezone($timezone);
2476            $this->recurrence->recurEnd->setTimezone($timezone);
2477        }
2478    }
2479
2480    public function getDuration()
2481    {
2482        if (isset($this->_duration)) {
2483            return $this->_duration;
2484        }
2485
2486        if ($this->start && $this->end) {
2487            $dur_day_match = Date_Calc::dateDiff($this->start->mday,
2488                                                 $this->start->month,
2489                                                 $this->start->year,
2490                                                 $this->end->mday,
2491                                                 $this->end->month,
2492                                                 $this->end->year);
2493            $dur_hour_match = $this->end->hour - $this->start->hour;
2494            $dur_min_match = $this->end->min - $this->start->min;
2495            while ($dur_min_match < 0) {
2496                $dur_min_match += 60;
2497                --$dur_hour_match;
2498            }
2499            while ($dur_hour_match < 0) {
2500                $dur_hour_match += 24;
2501                --$dur_day_match;
2502            }
2503        } else {
2504            $dur_day_match = 0;
2505            $dur_hour_match = 1;
2506            $dur_min_match = 0;
2507        }
2508
2509        $this->_duration = new stdClass;
2510        $this->_duration->day = $dur_day_match;
2511        $this->_duration->hour = $dur_hour_match;
2512        $this->_duration->min = $dur_min_match;
2513        $this->_duration->wholeDay = $this->isAllDay();
2514
2515        return $this->_duration;
2516    }
2517
2518    /**
2519     * Returns whether this event is a recurring event.
2520     *
2521     * @return boolean  True if this is a recurring event.
2522     */
2523    public function recurs()
2524    {
2525        return isset($this->recurrence) &&
2526            !$this->recurrence->hasRecurType(Horde_Date_Recurrence::RECUR_NONE) &&
2527            empty($this->baseid);
2528    }
2529
2530    /**
2531     * Returns a description of this event's recurring type.
2532     *
2533     * @return string  Human readable recurring type.
2534     */
2535    public function getRecurName()
2536    {
2537        if (empty($this->baseid)) {
2538            return $this->recurs()
2539                ? $this->recurrence->getRecurName()
2540                : _("No recurrence");
2541        } else {
2542            return _("Exception");
2543        }
2544    }
2545
2546    /**
2547     * Returns a correcty formatted exception date for recurring events and a
2548     * link to delete this exception.
2549     *
2550     * @param string $date  Exception in the format Ymd.
2551     *
2552     * @return string  The formatted date and delete link.
2553     */
2554    public function exceptionLink($date)
2555    {
2556        if (!preg_match('/(\d{4})(\d{2})(\d{2})/', $date, $match)) {
2557            return '';
2558        }
2559        $horde_date = new Horde_Date(array('year' => $match[1],
2560                                           'month' => $match[2],
2561                                           'mday' => $match[3]));
2562        $formatted = $horde_date->strftime($GLOBALS['prefs']->getValue('date_format'));
2563        return $formatted
2564            . Horde::url('edit.php')
2565            ->add(array('calendar' => $this->calendarType . '_' .$this->calendar,
2566                        'eventID' => $this->id,
2567                        'del_exception' => $date,
2568                        'url' => Horde_Util::getFormData('url')))
2569            ->link(array('title' => sprintf(_("Delete exception on %s"), $formatted)))
2570            . Horde::img('delete-small.png', _("Delete"))
2571            . '</a>';
2572    }
2573
2574    /**
2575     * Returns a list of exception dates for recurring events including links
2576     * to delete them.
2577     *
2578     * @return string  List of exception dates and delete links.
2579     */
2580    public function exceptionsList()
2581    {
2582        $exceptions = $this->recurrence->getExceptions();
2583        asort($exceptions);
2584        return implode(', ', array_map(array($this, 'exceptionLink'), $exceptions));
2585    }
2586
2587    /**
2588     * Returns a list of events that represent exceptions to this event's
2589     * recurrence series, if any. If this event does not recur, an empty array
2590     * is returned.
2591     *
2592     * @param boolean $flat  If true (the default), returns a flat array
2593     *                       containing Kronolith_Event objects. If false,
2594     *                       results are in the format of listEvents calls. @see
2595     *                       Kronolith::listEvents().
2596     *
2597     * @return array  An array of Kronolith_Event objects whose baseid property
2598     *                is equal to this event's uid. I.e., it is a bound
2599     *                exception.
2600     *
2601     * @since 4.2.2
2602     */
2603    public function boundExceptions($flat = true)
2604    {
2605        if (!$this->recurrence || !$this->uid) {
2606            return array();
2607        }
2608        $return = array();
2609        $search = new stdClass();
2610        $search->baseid = $this->uid;
2611        $results = $this->getDriver()->search($search);
2612
2613        if (!$flat) {
2614            return $results;
2615        }
2616
2617        foreach ($results as $days) {
2618            foreach ($days as $exception) {
2619                $return[] = $exception;
2620            }
2621        }
2622
2623        return $return;
2624    }
2625
2626    /**
2627     * Returns whether the event should be considered private.
2628     *
2629     * @param string $user  The current user. If omitted, uses the current user.
2630     *
2631     * @return boolean  Whether to consider the event as private.
2632     */
2633    public function isPrivate($user = null)
2634    {
2635        global $registry;
2636
2637        if ($user === null) {
2638            $user = $registry->getAuth();
2639        }
2640
2641        // Never private if private is not true or if the current user is the
2642        // event creator.
2643        if ((!$this->private || $this->creator == $user) &&
2644            $this->hasPermission(Horde_Perms::READ, $user)) {
2645            return false;
2646        }
2647
2648        return true;
2649    }
2650
2651    /**
2652     * Returns the title of this event, considering private flags.
2653     *
2654     * @param string $user  The current user.
2655     *
2656     * @return string  The title of this event.
2657     */
2658    public function getTitle($user = null)
2659    {
2660        if (!$this->initialized) {
2661            return '';
2662        }
2663
2664        return $this->isPrivate($user)
2665            ? _("busy")
2666            : (strlen($this->title) ? $this->title : _("[Unnamed event]"));
2667    }
2668
2669    /**
2670     * Returns the location of this event, considering private flags.
2671     *
2672     * @param string $user  The current user.
2673     *
2674     * @return string  The location of this event.
2675     */
2676    public function getLocation($user = null)
2677    {
2678        return $this->isPrivate($user) ? '' : $this->location;
2679    }
2680
2681    /**
2682     * Checks to see whether the specified attendee is associated with the
2683     * current event.
2684     *
2685     * @param string $email  The email address of the attendee.
2686     *
2687     * @return boolean  True if the specified attendee is present for this
2688     *                  event.
2689     */
2690    public function hasAttendee($email)
2691    {
2692        return isset($this->attendees[Horde_String::lower($email)]);
2693    }
2694
2695    /**
2696     * Adds a new attendee to the current event.
2697     *
2698     * This will overwrite an existing attendee if one exists with the same
2699     * email address.
2700     *
2701     * @param string $email        The email address of the attendee.
2702     * @param integer $attendance  The attendance code of the attendee.
2703     * @param integer $response    The response code of the attendee.
2704     * @param string $name         The name of the attendee.
2705     */
2706    public function addAttendee($email, $attendance, $response, $name = null)
2707    {
2708        if ($attendance == Kronolith::PART_IGNORE) {
2709            if (isset($this->attendees[$email])) {
2710                $attendance = $this->attendees[$email]['attendance'];
2711            } else {
2712                $attendance = Kronolith::PART_REQUIRED;
2713            }
2714        }
2715        if (empty($name) && isset($this->attendees[$email]) &&
2716            !empty($this->attendees[$email]['name'])) {
2717            $name = $this->attendees[$email]['name'];
2718        }
2719
2720        $this->attendees[$email] = array(
2721            'attendance' => $attendance,
2722            'response' => $response,
2723            'name' => $name
2724        );
2725    }
2726
2727    /**
2728     * Adds a single resource to this event.
2729     *
2730     * No validation or acceptence/denial is done here...it should be done
2731     * when saving the event.
2732     *
2733     * @param Kronolith_Resource $resource  The resource to add.
2734     */
2735    public function addResource($resource, $response)
2736    {
2737        $this->_resources[$resource->getId()] = array(
2738            'attendance' => Kronolith::PART_REQUIRED,
2739            'response' => $response,
2740            'name' => $resource->get('name'),
2741            'calendar' => $resource->get('calendar')
2742        );
2743    }
2744
2745    /**
2746     * Removes a resource from this event.
2747     *
2748     * @param Kronolith_Resource $resource  The resource to remove.
2749     */
2750    public function removeResource($resource)
2751    {
2752        if (isset($this->_resources[$resource->getId()])) {
2753            unset($this->_resources[$resource->getId()]);
2754        }
2755    }
2756
2757    /**
2758     * Returns all resources.
2759     *
2760     * @return array  A copy of the resources array.
2761     */
2762    public function getResources()
2763    {
2764        return $this->_resources;
2765    }
2766
2767    /**
2768     * Set the entire resource array. Only used when copying an Event.
2769     *
2770     * @param array  $resources  The resource array.
2771     * @since 4.2.6
2772     */
2773    public function setResources(array $resources)
2774    {
2775        $this->_resources = $resources;
2776    }
2777
2778    public function isAllDay()
2779    {
2780        return $this->allday ||
2781            ($this->start->hour == 0 && $this->start->min == 0 && $this->start->sec == 0 &&
2782             (($this->end->hour == 23 && $this->end->min == 59) ||
2783              ($this->end->hour == 0 && $this->end->min == 0 && $this->end->sec == 0 &&
2784               ($this->end->mday > $this->start->mday ||
2785                $this->end->month > $this->start->month ||
2786                $this->end->year > $this->start->year))));
2787    }
2788
2789    /**
2790     * Syncronizes tags from the tagging backend with the task storage backend,
2791     * if necessary.
2792     *
2793     * @param array $tags  Tags from the tagging backend.
2794     */
2795    public function synchronizeTags($tags)
2796    {
2797        if (isset($this->_internaltags)) {
2798            $lower_internaltags = array_map('Horde_String::lower', $this->_internaltags);
2799            $lower_tags = array_map('Horde_String::lower', $tags);
2800            usort($lower_tags, 'strcoll');
2801
2802            if (array_diff($lower_internaltags, $lower_tags)) {
2803                Kronolith::getTagger()->replaceTags(
2804                    $this->uid,
2805                    $this->_internaltags,
2806                    $this->_creator,
2807                    Kronolith_Tagger::TYPE_EVENT
2808                );
2809            }
2810            $this->_tags = $this->_internaltags;
2811        } else {
2812            $this->_tags = $tags;
2813        }
2814    }
2815
2816    /**
2817     * Reads form/post data and updates this event's properties.
2818     *
2819     * @param  Kronolith_Event|null $existing  If this is an exception event
2820     *                                         this is taken as the base event.
2821     *                                         @since 4.2.6
2822     *
2823     */
2824    public function readForm(Kronolith_Event $existing = null)
2825    {
2826        global $prefs, $session;
2827
2828        // Event owner.
2829        $targetcalendar = Horde_Util::getFormData('targetcalendar');
2830        if (strpos($targetcalendar, '\\')) {
2831            list(, $this->creator) = explode('\\', $targetcalendar, 2);
2832        } elseif (!isset($this->_id)) {
2833            $this->creator = $GLOBALS['registry']->getAuth();
2834        }
2835
2836        // Basic fields.
2837        $this->title = Horde_Util::getFormData('title', $this->title);
2838        $this->description = Horde_Util::getFormData('description', $this->description);
2839        $this->location = Horde_Util::getFormData('location', $this->location);
2840        $this->timezone = Horde_Util::getFormData('timezone', $this->timezone);
2841        $this->private = (bool)Horde_Util::getFormData('private');
2842
2843        // URL.
2844        $url = Horde_Util::getFormData('eventurl', $this->url);
2845        if (strlen($url)) {
2846            // Analyze and re-construct.
2847            $url = @parse_url($url);
2848            if ($url) {
2849                if (function_exists('http_build_url')) {
2850                    if (empty($url['path'])) {
2851                        $url['path'] = '/';
2852                    }
2853                    $url = http_build_url($url);
2854                } else {
2855                    $new_url = '';
2856                    if (isset($url['scheme'])) {
2857                        $new_url .= $url['scheme'] . '://';
2858                    }
2859                    if (isset($url['user'])) {
2860                        $new_url .= $url['user'];
2861                        if (isset($url['pass'])) {
2862                            $new_url .= ':' . $url['pass'];
2863                        }
2864                        $new_url .= '@';
2865                    }
2866                    if (isset($url['host'])) {
2867                        // Convert IDN hosts to ASCII.
2868                        if (function_exists('idn_to_ascii')) {
2869                            $url['host'] = @idn_to_ascii($url['host']);
2870                        } elseif (Horde_Mime::is8bit($url['host'])) {
2871                            //throw new Kronolith_Exception(_("Invalid character in URL."));
2872                            $url['host'] = '';
2873                        }
2874                        $new_url .= $url['host'];
2875                    }
2876                    if (isset($url['path'])) {
2877                        $new_url .= $url['path'];
2878                    }
2879                    if (isset($url['query'])) {
2880                        $new_url .= '?' . $url['query'];
2881                    }
2882                    if (isset($url['fragment'])) {
2883                        $new_url .= '#' . $url['fragment'];
2884                    }
2885                    $url = $new_url;
2886                }
2887            }
2888        }
2889        $this->url = $url;
2890
2891        // Status.
2892        $this->status = Horde_Util::getFormData('status', $this->status);
2893
2894        // Attendees.
2895        $attendees = $session->get('kronolith', 'attendees', Horde_Session::TYPE_ARRAY);
2896        if (!is_null($newattendees = Horde_Util::getFormData('attendees'))) {
2897            $newattendees = Kronolith::parseAttendees(trim($newattendees));
2898            foreach ($newattendees as $email => $attendee) {
2899                if (!isset($attendees[$email])) {
2900                    $attendees[$email] = $attendee;
2901                }
2902            }
2903            foreach (array_keys($attendees) as $email) {
2904                if (!isset($newattendees[$email])) {
2905                    unset($attendees[$email]);
2906                }
2907            }
2908        }
2909        $this->attendees = $attendees;
2910
2911        // Event start.
2912        $allDay = Horde_Util::getFormData('whole_day');
2913        if ($start_date = Horde_Util::getFormData('start_date')) {
2914            // From ajax interface.
2915            $this->start = Kronolith::parseDate($start_date . ' ' . Horde_Util::getFormData('start_time'), true, $this->timezone);
2916            if ($allDay) {
2917                $this->start->hour = $this->start->min = $this->start->sec = 0;
2918            }
2919        } elseif ($start = Horde_Util::getFormData('start')) {
2920            // From traditional interface.
2921            $start_year = $start['year'];
2922            $start_month = $start['month'];
2923            $start_day = $start['day'];
2924            $start_hour = Horde_Util::getFormData('start_hour');
2925            $start_min = Horde_Util::getFormData('start_min');
2926            $am_pm = Horde_Util::getFormData('am_pm');
2927
2928            if (!$prefs->getValue('twentyFour')) {
2929                if ($am_pm == 'PM') {
2930                    if ($start_hour != 12) {
2931                        $start_hour += 12;
2932                    }
2933                } elseif ($start_hour == 12) {
2934                    $start_hour = 0;
2935                }
2936            }
2937
2938            if (Horde_Util::getFormData('end_or_dur') == 1) {
2939                if ($allDay) {
2940                    $start_hour = 0;
2941                    $start_min = 0;
2942                    $dur_day = 0;
2943                    $dur_hour = 24;
2944                    $dur_min = 0;
2945                } else {
2946                    $dur_day = (int)Horde_Util::getFormData('dur_day');
2947                    $dur_hour = (int)Horde_Util::getFormData('dur_hour');
2948                    $dur_min = (int)Horde_Util::getFormData('dur_min');
2949                }
2950            }
2951
2952            $this->start = new Horde_Date(array('hour' => $start_hour,
2953                                                'min' => $start_min,
2954                                                'month' => $start_month,
2955                                                'mday' => $start_day,
2956                                                'year' => $start_year),
2957                                          $this->timezone);
2958        }
2959
2960        // Event end.
2961        if ($end_date = Horde_Util::getFormData('end_date')) {
2962            // From ajax interface.
2963            $this->end = Kronolith::parseDate($end_date . ' ' . Horde_Util::getFormData('end_time'), true, $this->timezone);
2964            if ($allDay) {
2965                $this->end->hour = $this->end->min = $this->end->sec = 0;
2966                $this->end->mday++;
2967            }
2968        } elseif (Horde_Util::getFormData('end_or_dur') == 1) {
2969            // Event duration from traditional interface.
2970            $this->end = new Horde_Date(array('hour' => $start_hour + $dur_hour,
2971                                              'min' => $start_min + $dur_min,
2972                                              'month' => $start_month,
2973                                              'mday' => $start_day + $dur_day,
2974                                              'year' => $start_year));
2975        } elseif ($end = Horde_Util::getFormData('end')) {
2976            // From traditional interface.
2977            $end_year = $end['year'];
2978            $end_month = $end['month'];
2979            $end_day = $end['day'];
2980            $end_hour = Horde_Util::getFormData('end_hour');
2981            $end_min = Horde_Util::getFormData('end_min');
2982            $end_am_pm = Horde_Util::getFormData('end_am_pm');
2983
2984            if (!$prefs->getValue('twentyFour')) {
2985                if ($end_am_pm == 'PM') {
2986                    if ($end_hour != 12) {
2987                        $end_hour += 12;
2988                    }
2989                } elseif ($end_hour == 12) {
2990                    $end_hour = 0;
2991                }
2992            }
2993
2994            $this->end = new Horde_Date(array('hour' => $end_hour,
2995                                              'min' => $end_min,
2996                                              'month' => $end_month,
2997                                              'mday' => $end_day,
2998                                              'year' => $end_year),
2999                                        $this->timezone);
3000            if ($this->end->compareDateTime($this->start) < 0) {
3001                $this->end = new Horde_Date($this->start);
3002            }
3003        }
3004
3005        $this->allday = false;
3006
3007        // Alarm.
3008        if (!is_null($alarm = Horde_Util::getFormData('alarm'))) {
3009            if ($alarm) {
3010                $value = Horde_Util::getFormData('alarm_value');
3011                $unit = Horde_Util::getFormData('alarm_unit');
3012                if ($value == 0) {
3013                    $value = $unit = 1;
3014                }
3015                $this->alarm = $value * $unit;
3016                // Notification.
3017                if (Horde_Util::getFormData('alarm_change_method')) {
3018                    $types = Horde_Util::getFormData('event_alarms');
3019                    $methods = array();
3020                    if (!empty($types)) {
3021                        foreach ($types as $type) {
3022                            $methods[$type] = array();
3023                            switch ($type){
3024                            case 'notify':
3025                                $methods[$type]['sound'] = Horde_Util::getFormData('event_alarms_sound');
3026                                break;
3027                            case 'mail':
3028                                $methods[$type]['email'] = Horde_Util::getFormData('event_alarms_email');
3029                                break;
3030                            case 'popup':
3031                                break;
3032                            }
3033                        }
3034                    }
3035                    $this->methods = $methods;
3036                } else {
3037                    $this->methods = array();
3038                }
3039            } else {
3040                $this->alarm = 0;
3041                $this->methods = array();
3042            }
3043        }
3044
3045        // Recurrence.
3046        $this->recurrence = $this->readRecurrenceForm(
3047            $this->start, $this->timezone, $this->recurrence);
3048
3049        // Convert to local timezone.
3050        $this->setTimezone(false);
3051
3052        $this->_handleResources($existing);
3053
3054        // Tags.
3055        $this->tags = Horde_Util::getFormData('tags', $this->tags);
3056
3057        // Geolocation
3058        if (Horde_Util::getFormData('lat') && Horde_Util::getFormData('lon')) {
3059            $this->geoLocation = array('lat' => Horde_Util::getFormData('lat'),
3060                                       'lon' => Horde_Util::getFormData('lon'),
3061                                       'zoom' => Horde_Util::getFormData('zoom'));
3062        }
3063
3064        $this->initialized = true;
3065    }
3066
3067    static public function readRecurrenceForm($start, $timezone,
3068                                              $recurrence = null)
3069    {
3070        $recur = Horde_Util::getFormData('recur');
3071        if (!strlen($recur)) {
3072            return $recurrence;
3073        }
3074        if (!isset($recurrence)) {
3075            $recurrence = new Horde_Date_Recurrence($start);
3076        } else {
3077            $recurrence->setRecurStart($start);
3078        }
3079        if (Horde_Util::getFormData('recur_end_type') == 'date') {
3080            $end_date = Horde_Util::getFormData('recur_end_date', false);
3081            if ($end_date !== false) {
3082                // From ajax interface.
3083                if (empty($end_date)) {
3084                    throw new Kronolith_Exception("Missing required end date of recurrence.");
3085                }
3086                $date_ob = Kronolith::parseDate($end_date, false);
3087                $recur_enddate = array(
3088                    'year'  => $date_ob->year,
3089                    'month' => $date_ob->month,
3090                    'day'  => $date_ob->mday);
3091            } else {
3092                // From traditional interface.
3093                $recur_enddate = Horde_Util::getFormData('recur_end');
3094            }
3095            if ($recurrence->hasRecurEnd()) {
3096                $recurEnd = $recurrence->recurEnd;
3097                $recurEnd->month = $recur_enddate['month'];
3098                $recurEnd->mday = $recur_enddate['day'];
3099                $recurEnd->year = $recur_enddate['year'];
3100            } else {
3101                $recurEnd = new Horde_Date(
3102                    array('hour' => 23,
3103                          'min' => 59,
3104                          'sec' => 59,
3105                          'month' => $recur_enddate['month'],
3106                          'mday' => $recur_enddate['day'],
3107                          'year' => $recur_enddate['year']),
3108                    $timezone);
3109            }
3110            $recurrence->setRecurEnd($recurEnd);
3111        } elseif (Horde_Util::getFormData('recur_end_type') == 'count') {
3112            $recurrence->setRecurCount(Horde_Util::getFormData('recur_count'));
3113        } elseif (Horde_Util::getFormData('recur_end_type') == 'none') {
3114            $recurrence->setRecurCount(0);
3115            $recurrence->setRecurEnd(null);
3116        }
3117
3118        $recurrence->setRecurType($recur);
3119        switch ($recur) {
3120        case Horde_Date_Recurrence::RECUR_DAILY:
3121            $recurrence->setRecurInterval(Horde_Util::getFormData('recur_daily_interval', 1));
3122            break;
3123
3124        case Horde_Date_Recurrence::RECUR_WEEKLY:
3125            $weekly = Horde_Util::getFormData('weekly');
3126            $weekdays = 0;
3127            if (is_array($weekly)) {
3128                foreach ($weekly as $day) {
3129                    $weekdays |= $day;
3130                }
3131            }
3132
3133            if ($weekdays == 0) {
3134                // Sunday starts at 0.
3135                switch ($start->dayOfWeek()) {
3136                case 0: $weekdays |= Horde_Date::MASK_SUNDAY; break;
3137                case 1: $weekdays |= Horde_Date::MASK_MONDAY; break;
3138                case 2: $weekdays |= Horde_Date::MASK_TUESDAY; break;
3139                case 3: $weekdays |= Horde_Date::MASK_WEDNESDAY; break;
3140                case 4: $weekdays |= Horde_Date::MASK_THURSDAY; break;
3141                case 5: $weekdays |= Horde_Date::MASK_FRIDAY; break;
3142                case 6: $weekdays |= Horde_Date::MASK_SATURDAY; break;
3143                }
3144            }
3145
3146            $recurrence->setRecurInterval(Horde_Util::getFormData('recur_weekly_interval', 1));
3147            $recurrence->setRecurOnDay($weekdays);
3148            break;
3149
3150        case Horde_Date_Recurrence::RECUR_MONTHLY_DATE:
3151            switch (Horde_Util::getFormData('recur_monthly_scheme')) {
3152            case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY:
3153                $recurrence->setRecurType(Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY);
3154            case Horde_Date_Recurrence::RECUR_MONTHLY_DATE:
3155                $recurrence->setRecurInterval(
3156                    Horde_Util::getFormData('recur_monthly')
3157                        ? 1
3158                        : Horde_Util::getFormData('recur_monthly_interval', 1)
3159                );
3160                break;
3161            default:
3162                $recurrence->setRecurInterval(Horde_Util::getFormData('recur_day_of_month_interval', 1));
3163                break;
3164            }
3165            break;
3166
3167        case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY:
3168            $recurrence->setRecurInterval(Horde_Util::getFormData('recur_week_of_month_interval', 1));
3169            break;
3170
3171        case Horde_Date_Recurrence::RECUR_YEARLY_DATE:
3172            switch (Horde_Util::getFormData('recur_yearly_scheme')) {
3173            case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY:
3174            case Horde_Date_Recurrence::RECUR_YEARLY_DAY:
3175                $recurrence->setRecurType(Horde_Util::getFormData('recur_yearly_scheme'));
3176            case Horde_Date_Recurrence::RECUR_YEARLY_DATE:
3177                $recurrence->setRecurInterval(
3178                    Horde_Util::getFormData('recur_yearly')
3179                        ? 1
3180                        : Horde_Util::getFormData('recur_yearly_interval', 1)
3181                );
3182                break;
3183            default:
3184                $recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly_interval', 1));
3185                break;
3186            }
3187            break;
3188
3189        case Horde_Date_Recurrence::RECUR_YEARLY_DAY:
3190            $recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly_day_interval', $yearly_interval));
3191            break;
3192
3193        case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY:
3194            $recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly_weekday_interval', $yearly_interval));
3195            break;
3196        }
3197
3198        foreach (array('exceptions', 'completions') as $what) {
3199            if ($data = Horde_Util::getFormData($what)) {
3200                if (!is_array($data)) {
3201                    $data = explode(',', $data);
3202                }
3203                foreach ($data as $date) {
3204                    list($year, $month, $mday) = sscanf($date, '%04d%02d%02d');
3205                    if ($what == 'exceptions') {
3206                        $recurrence->addException($year, $month, $mday);
3207                    } else {
3208                        $recurrence->addCompletion($year, $month, $mday);
3209                    }
3210                }
3211            }
3212        }
3213
3214        return $recurrence;
3215    }
3216
3217    /**
3218     * Handles updating/saving this event's resources. Unless this event recurs,
3219     * this will delete this event from any resource calendars that are no
3220     * longer needed (as when a resource is removed from an existing event). If
3221     * this event is an exception, i.e., contains a baseid, AND $existing is
3222     * provided, the resources from the original event are used for purposes
3223     * of determining any resources that need to be removed.
3224     *
3225     *
3226     * @param  Kronolith_Event|null $existing  An existing base event.
3227     * @since 4.2.6
3228     */
3229    protected function _handleResources(Kronolith_Event $existing = null)
3230    {
3231        global $session;
3232
3233        if (Horde_Util::getFormData('isajax', false)) {
3234            $resources = array();
3235        } else {
3236            $resources = $session->get('kronolith', 'resources', Horde_Session::TYPE_ARRAY);
3237        }
3238
3239        $existingResources = $this->_resources;
3240        $newresources = Horde_Util::getFormData('resources');
3241        if (!empty($newresources)) {
3242            foreach (explode(',', $newresources) as $id) {
3243                try {
3244                    $resource = Kronolith::getDriver('Resource')->getResource($id);
3245                } catch (Kronolith_Exception $e) {
3246                    $GLOBALS['notification']->push($e->getMessage(), 'horde.error');
3247                    continue;
3248                }
3249                if (!($resource instanceof Kronolith_Resource_Group) ||
3250                    $resource->isFree($this)) {
3251                    $resources[$resource->getId()] = array(
3252                        'attendance' => Kronolith::PART_REQUIRED,
3253                        'response'   => Kronolith::RESPONSE_NONE,
3254                        'name'       => $resource->get('name')
3255                    );
3256                } else {
3257                    $GLOBALS['notification']->push(_("No resources from this group were available"), 'horde.error');
3258                }
3259            }
3260        }
3261        $this->_resources = $resources;
3262
3263
3264        // Have the base event, and this is an exception so we must
3265        // match the recurrence in the resource's copy of the base event.
3266        if (!empty($existing) && $existing->recurs() && !$this->recurs()) {
3267            foreach ($existing->getResources() as $rid => $data) {
3268                $resource = Kronolith::getDriver('Resource')->getResource($key);
3269                $r_event = Kronolith::getDriver('Resource')->getByUID($existing->uid, $resource->calendar);
3270                $r_event->recurrence = $event->recurrence;
3271                $r_event->save();
3272            }
3273        }
3274
3275        // If we don't recur, check for removal of any resources so we can
3276        // update those resources' calendars.
3277        if (!$this->recurs()) {
3278            $merged = $existingResources + $this->_resources;
3279            $delete = array_diff(array_keys($existingResources), array_keys($this->_resources));
3280            foreach ($delete as $key) {
3281                // Resource might be declined, in which case it won't have the event
3282                // on it's calendar.
3283                if ($merged[$key]['response'] != Kronolith::RESPONSE_DECLINED) {
3284                    try {
3285                        Kronolith::getDriver('Resource')
3286                            ->getResource($key)
3287                            ->removeEvent($this);
3288                    } catch (Kronolith_Exception $e) {
3289                        $GLOBALS['notification']->push('foo', 'horde.error');
3290                    }
3291                }
3292            }
3293        }
3294    }
3295
3296    public function html($property)
3297    {
3298        global $prefs;
3299
3300        $options = array();
3301        $attributes = '';
3302        $sel = false;
3303        $label = '';
3304
3305        switch ($property) {
3306        case 'start[year]':
3307            return  '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . _("Start Year") . '</label>' .
3308                '<input name="' . $property . '" value="' . $this->start->year .
3309                '" type="text"' .
3310                ' id="' . $this->_formIDEncode($property) . '" size="4" maxlength="4" />';
3311
3312        case 'start[month]':
3313            $sel = $this->start->month;
3314            for ($i = 1; $i < 13; ++$i) {
3315                $options[$i] = strftime('%b', mktime(1, 1, 1, $i, 1));
3316            }
3317            $label = _("Start Month");
3318            break;
3319
3320        case 'start[day]':
3321            $sel = $this->start->mday;
3322            for ($i = 1; $i < 32; ++$i) {
3323                $options[$i] = $i;
3324            }
3325            $label = _("Start Day");
3326            break;
3327
3328        case 'start_hour':
3329            $sel = $this->start->format($prefs->getValue('twentyFour') ? 'G' : 'g');
3330            $hour_min = $prefs->getValue('twentyFour') ? 0 : 1;
3331            $hour_max = $prefs->getValue('twentyFour') ? 24 : 13;
3332            for ($i = $hour_min; $i < $hour_max; ++$i) {
3333                $options[$i] = $i;
3334            }
3335            $label = _("Start Hour");
3336            break;
3337
3338        case 'start_min':
3339            $sel = sprintf('%02d', $this->start->min);
3340            for ($i = 0; $i < 12; ++$i) {
3341                $min = sprintf('%02d', $i * 5);
3342                $options[$min] = $min;
3343            }
3344            $label = _("Start Minute");
3345            break;
3346
3347        case 'end[year]':
3348            return  '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . _("End Year") . '</label>' .
3349                '<input name="' . $property . '" value="' . $this->end->year .
3350                '" type="text"' .
3351                ' id="' . $this->_formIDEncode($property) . '" size="4" maxlength="4" />';
3352
3353        case 'end[month]':
3354            $sel = $this->end ? $this->end->month : $this->start->month;
3355            for ($i = 1; $i < 13; ++$i) {
3356                $options[$i] = strftime('%b', mktime(1, 1, 1, $i, 1));
3357            }
3358            $label = _("End Month");
3359            break;
3360
3361        case 'end[day]':
3362            $sel = $this->end ? $this->end->mday : $this->start->mday;
3363            for ($i = 1; $i < 32; ++$i) {
3364                $options[$i] = $i;
3365            }
3366            $label = _("End Day");
3367            break;
3368
3369        case 'end_hour':
3370            $sel = $this->end
3371                ? $this->end->format($prefs->getValue('twentyFour') ? 'G' : 'g')
3372                : $this->start->format($prefs->getValue('twentyFour') ? 'G' : 'g') + 1;
3373            $hour_min = $prefs->getValue('twentyFour') ? 0 : 1;
3374            $hour_max = $prefs->getValue('twentyFour') ? 24 : 13;
3375            for ($i = $hour_min; $i < $hour_max; ++$i) {
3376                $options[$i] = $i;
3377            }
3378            $label = _("End Hour");
3379            break;
3380
3381        case 'end_min':
3382            $sel = $this->end ? $this->end->min : $this->start->min;
3383            $sel = sprintf('%02d', $sel);
3384            for ($i = 0; $i < 12; ++$i) {
3385                $min = sprintf('%02d', $i * 5);
3386                $options[$min] = $min;
3387            }
3388            $label = _("End Minute");
3389            break;
3390
3391        case 'dur_day':
3392            $dur = $this->getDuration();
3393            return  '<label for="' . $property . '" class="hidden">' . _("Duration Day") . '</label>' .
3394                '<input name="' . $property . '" value="' . $dur->day .
3395                '" type="text"' .
3396                ' id="' . $property . '" size="4" maxlength="4" />';
3397
3398        case 'dur_hour':
3399            $dur = $this->getDuration();
3400            $sel = $dur->hour;
3401            for ($i = 0; $i < 24; ++$i) {
3402                $options[$i] = $i;
3403            }
3404            $label = _("Duration Hour");
3405            break;
3406
3407        case 'dur_min':
3408            $dur = $this->getDuration();
3409            $sel = $dur->min;
3410            for ($i = 0; $i < 13; ++$i) {
3411                $min = sprintf('%02d', $i * 5);
3412                $options[$min] = $min;
3413            }
3414            $label = _("Duration Minute");
3415            break;
3416
3417        case 'recur_end[year]':
3418            if ($this->end) {
3419                $end = ($this->recurs() && $this->recurrence->hasRecurEnd())
3420                        ? $this->recurrence->recurEnd->year
3421                        : $this->end->year;
3422            } else {
3423                $end = $this->start->year;
3424            }
3425            return  '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . _("Recurrence End Year") . '</label>' .
3426                '<input name="' . $property . '" value="' . $end .
3427                '" type="text"' .
3428                ' id="' . $this->_formIDEncode($property) . '" size="4" maxlength="4" />';
3429
3430        case 'recur_end[month]':
3431            if ($this->end) {
3432                $sel = ($this->recurs() && $this->recurrence->hasRecurEnd())
3433                    ? $this->recurrence->recurEnd->month
3434                    : $this->end->month;
3435            } else {
3436                $sel = $this->start->month;
3437            }
3438            for ($i = 1; $i < 13; ++$i) {
3439                $options[$i] = strftime('%b', mktime(1, 1, 1, $i, 1));
3440            }
3441            $label = _("Recurrence End Month");
3442            break;
3443
3444        case 'recur_end[day]':
3445            if ($this->end) {
3446                $sel = ($this->recurs() && $this->recurrence->hasRecurEnd())
3447                    ? $this->recurrence->recurEnd->mday
3448                    : $this->end->mday;
3449            } else {
3450                $sel = $this->start->mday;
3451            }
3452            for ($i = 1; $i < 32; ++$i) {
3453                $options[$i] = $i;
3454            }
3455            $label = _("Recurrence End Day");
3456            break;
3457        }
3458
3459        if (!$this->_varRenderer) {
3460            $this->_varRenderer = Horde_Core_Ui_VarRenderer::factory('Html');
3461        }
3462
3463        return '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . $label . '</label>' .
3464            '<select name="' . $property . '"' . $attributes . ' id="' . $this->_formIDEncode($property) . '">' .
3465            $this->_varRenderer->selectOptions($options, $sel) .
3466            '</select>';
3467    }
3468
3469    /**
3470     * @param array $params
3471     *
3472     * @return Horde_Url
3473     */
3474    public function getViewUrl($params = array(), $full = false, $encoded = true)
3475    {
3476        $params['eventID'] = $this->id;
3477        $params['calendar'] = $this->calendar;
3478        $params['type'] = $this->calendarType;
3479
3480        return Horde::url('event.php', $full)->setRaw(!$encoded)->add($params);
3481    }
3482
3483    /**
3484     * @param array $params
3485     *
3486     * @return Horde_Url
3487     */
3488    public function getEditUrl($params = array(), $full = false)
3489    {
3490        $params['view'] = 'EditEvent';
3491        $params['eventID'] = $this->id;
3492        $params['calendar'] = $this->calendar;
3493        $params['type'] = $this->calendarType;
3494
3495        return Horde::url('event.php', $full)->add($params);
3496    }
3497
3498    /**
3499     * @param array $params
3500     *
3501     * @return Horde_Url
3502     */
3503    public function getDeleteUrl($params = array(), $full = false)
3504    {
3505        $params['view'] = 'DeleteEvent';
3506        $params['eventID'] = $this->id;
3507        $params['calendar'] = $this->calendar;
3508        $params['type'] = $this->calendarType;
3509
3510        return Horde::url('event.php', $full)->add($params);
3511    }
3512
3513    /**
3514     * @param array $params
3515     *
3516     * @return Horde_Url
3517     */
3518    public function getExportUrl($params = array(), $full = false)
3519    {
3520        $params['view'] = 'ExportEvent';
3521        $params['eventID'] = $this->id;
3522        $params['calendar'] = $this->calendar;
3523        $params['type'] = $this->calendarType;
3524
3525        return Horde::url('event.php', $full)->add($params);
3526    }
3527
3528    public function getLink($datetime = null, $icons = true, $from_url = null,
3529                            $full = false, $encoded = true)
3530    {
3531        global $prefs;
3532
3533        if (is_null($datetime)) {
3534            $datetime = $this->start;
3535        }
3536        if (is_null($from_url)) {
3537            $from_url = Horde::selfUrl(true, false, true);
3538        }
3539
3540        $event_title = $this->getTitle();
3541        $view_url = $this->getViewUrl(array('datetime' => $datetime->strftime('%Y%m%d%H%M%S'), 'url' => $from_url), $full, $encoded);
3542        $read_permission = $this->hasPermission(Horde_Perms::READ);
3543
3544        $link = '<span' . $this->getCSSColors() . '>';
3545        if ($read_permission && $view_url) {
3546            $link .= Horde::linkTooltip($view_url,
3547                                       $event_title,
3548                                       $this->getStatusClass(),
3549                                       '',
3550                                       '',
3551                                       $this->getTooltip(),
3552                                       '',
3553                                       array('style' => $this->getCSSColors(false)));
3554        }
3555        $link .= htmlspecialchars($event_title);
3556        if ($read_permission && $view_url) {
3557            $link .= '</a>';
3558        }
3559
3560        if ($icons && $prefs->getValue('show_icons')) {
3561            $icon_color = $this->_foregroundColor == '#000' ? '000' : 'fff';
3562            $status = '';
3563            if ($this->alarm) {
3564                if ($this->alarm % 10080 == 0) {
3565                    $alarm_value = $this->alarm / 10080;
3566                    $title = sprintf(ngettext("Alarm %d week before", "Alarm %d weeks before", $alarm_value), $alarm_value);
3567                } elseif ($this->alarm % 1440 == 0) {
3568                    $alarm_value = $this->alarm / 1440;
3569                    $title = sprintf(ngettext("Alarm %d day before", "Alarm %d days before", $alarm_value), $alarm_value);
3570                } elseif ($this->alarm % 60 == 0) {
3571                    $alarm_value = $this->alarm / 60;
3572                    $title = sprintf(ngettext("Alarm %d hour before", "Alarm %d hours before", $alarm_value), $alarm_value);
3573                } else {
3574                    $alarm_value = $this->alarm;
3575                    $title = sprintf(ngettext("Alarm %d minute before", "Alarm %d minutes before", $alarm_value), $alarm_value);
3576                }
3577                $status .= Horde::fullSrcImg('alarm-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconAlarm')));
3578            }
3579
3580            if ($this->recurs()) {
3581                $title = Kronolith::recurToString($this->recurrence->getRecurType());
3582                $status .= Horde::fullSrcImg('recur-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconRecur')));
3583            } elseif ($this->baseid) {
3584                $title = _("Exception");
3585                $status .= Horde::fullSrcImg('exception-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconRecur')));
3586            }
3587
3588            if ($this->private) {
3589                $title = _("Private event");
3590                $status .= Horde::fullSrcImg('private-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconPrivate')));
3591            }
3592
3593            if (!empty($this->attendees)) {
3594                $status .= Horde::fullSrcImg('attendees-' . $icon_color . '.png', array('attr' => array('alt' => _("Meeting"), 'title' => _("Meeting"), 'class' => 'iconPeople')));
3595            }
3596
3597            $space = ' ';
3598            if (!empty($this->icon)) {
3599                $link = $status . ' <img class="kronolithEventIcon" src="' . $this->icon . '" /> ' . $link;
3600            } elseif (!empty($status)) {
3601                $link .= ' ' . $status;
3602                $space = '';
3603            }
3604
3605            if ((!$this->private ||
3606                 $this->creator == $GLOBALS['registry']->getAuth()) &&
3607                Kronolith::getDefaultCalendar(Horde_Perms::EDIT)) {
3608                $url = $this->getEditUrl(
3609                    array('datetime' => $datetime->strftime('%Y%m%d%H%M%S'),
3610                          'url' => $from_url),
3611                    $full);
3612                if ($url) {
3613                    $link .= $space
3614                        . $url->link(array('title' => sprintf(_("Edit %s"), $event_title),
3615                                           'class' => 'iconEdit'))
3616                        . Horde::fullSrcImg('edit-' . $icon_color . '.png',
3617                                            array('attr' => array('alt' => _("Edit"))))
3618                        . '</a>';
3619                    $space = '';
3620                }
3621            }
3622            if ($this->hasPermission(Horde_Perms::DELETE)) {
3623                $url = $this->getDeleteUrl(
3624                    array('datetime' => $datetime->strftime('%Y%m%d%H%M%S'),
3625                          'url' => $from_url),
3626                    $full);
3627                if ($url) {
3628                    $link .= $space
3629                        . $url->link(array('title' => sprintf(_("Delete %s"), $event_title),
3630                                           'class' => 'iconDelete'))
3631                        . Horde::fullSrcImg('delete-' . $icon_color . '.png',
3632                                            array('attr' => array('alt' => _("Delete"))))
3633                        . '</a>';
3634                }
3635            }
3636        }
3637
3638        return $link . '</span>';
3639    }
3640
3641    /**
3642     * Returns the CSS color definition for this event.
3643     *
3644     * @param boolean $with_attribute  Whether to wrap the colors inside a
3645     *                                 "style" attribute.
3646     *
3647     * @return string  A CSS string with color definitions.
3648     */
3649    public function getCSSColors($with_attribute = true)
3650    {
3651        $css = 'background-color:' . $this->_backgroundColor . ';color:' . $this->_foregroundColor;
3652        if ($with_attribute) {
3653            $css = ' style="' . $css . '"';
3654        }
3655        return $css;
3656    }
3657
3658    /**
3659     * @return string  A tooltip for quick descriptions of this event.
3660     */
3661    public function getTooltip()
3662    {
3663        $tooltip = $this->getTimeRange()
3664            . "\n" . sprintf(_("Owner: %s"), ($this->creator == $GLOBALS['registry']->getAuth() ?
3665                                              _("Me") : Kronolith::getUserName($this->creator)));
3666
3667        if (!$this->isPrivate()) {
3668            if ($this->location) {
3669                $tooltip .= "\n" . _("Location") . ': ' . $this->location;
3670            }
3671
3672            if ($this->description) {
3673                $tooltip .= "\n\n" . Horde_String::wrap($this->description);
3674            }
3675        }
3676
3677        return $tooltip;
3678    }
3679
3680    /**
3681     * @return string  The time range of the event ("All Day", "1:00pm-3:00pm",
3682     *                 "08:00-22:00").
3683     */
3684    public function getTimeRange()
3685    {
3686        if ($this->isAllDay()) {
3687            return _("All day");
3688        } elseif (($cmp = $this->start->compareDate($this->end)) > 0) {
3689            $df = $GLOBALS['prefs']->getValue('date_format');
3690            if ($cmp > 0) {
3691                return $this->end->strftime($df) . '-'
3692                    . $this->start->strftime($df);
3693            } else {
3694                return $this->start->strftime($df) . '-'
3695                    . $this->end->strftime($df);
3696            }
3697        } else {
3698            $twentyFour = $GLOBALS['prefs']->getValue('twentyFour');
3699            return $this->start->format($twentyFour ? 'G:i' : 'g:ia')
3700                . '-'
3701                . $this->end->format($twentyFour ? 'G:i' : 'g:ia');
3702        }
3703    }
3704
3705    /**
3706     * @return string  The CSS class for the event based on its status.
3707     */
3708    public function getStatusClass()
3709    {
3710        switch ($this->status) {
3711        case Kronolith::STATUS_CANCELLED:
3712            return 'kronolith-event-cancelled';
3713
3714        case Kronolith::STATUS_TENTATIVE:
3715        case Kronolith::STATUS_FREE:
3716            return 'kronolith-event-tentative';
3717        }
3718    }
3719
3720    protected function _formIDEncode($id)
3721    {
3722        return str_replace(array('[', ']'),
3723                           array('_', ''),
3724                           $id);
3725    }
3726
3727    /**
3728     * Ensure the given string is valid UTF-8.
3729     *
3730     * @param string $text  The string to ensure contains no invalid UTF-8 sequences.
3731     *
3732     * @return string|boolean  The valid UTF-8 string, possibly with illegal sequences removed.
3733     */
3734    protected function _ensureUtf8($text)
3735    {
3736        if (Horde_String::validUtf8($text)) {
3737            return $text;
3738        }
3739
3740        return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $text);
3741    }
3742}
3743