1<?php
2/**
3 * Defines the AJAX actions used in Kronolith.
4 *
5 * Copyright 2012-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   Michael Slusarz <slusarz@horde.org>
11 * @author   Jan Schneider <jan@horde.org>
12 * @author   Gonçalo Queirós <mail@goncaloqueiros.net>
13 * @category Horde
14 * @license  http://www.horde.org/licenses/gpl GPL
15 * @package  Kronolith
16 */
17class Kronolith_Ajax_Application_Handler extends Horde_Core_Ajax_Application_Handler
18{
19    protected $_external = array('embed');
20
21    /**
22     * Just polls for alarm messages and keeps session fresh for now.
23     */
24    public function poll()
25    {
26        return false;
27    }
28
29    /**
30     * Returns a list of all calendars.
31     */
32    public function listCalendars()
33    {
34        Kronolith::initialize();
35        $all_external_calendars = $GLOBALS['calendar_manager']->get(Kronolith::ALL_EXTERNAL_CALENDARS);
36        $result = new stdClass;
37        $auth_name = $GLOBALS['registry']->getAuth();
38
39        // Calendars. Do some twisting to sort own calendar before shared
40        // calendars.
41        foreach (array(true, false) as $my) {
42            foreach ($GLOBALS['calendar_manager']->get(Kronolith::ALL_CALENDARS) as $id => $calendar) {
43                $owner = ($auth_name && ($calendar->owner() == $auth_name));
44                if (($my && $owner) || (!$my && !$owner)) {
45                    $result->calendars['internal'][$id] = $calendar->toHash();
46                }
47            }
48
49            // Tasklists
50            if (Kronolith::hasApiPermission('tasks')) {
51                foreach ($GLOBALS['registry']->tasks->listTasklists($my, Horde_Perms::SHOW, false) as $id => $tasklist) {
52                    if (isset($all_external_calendars['tasks/' . $id])) {
53                        $owner = ($auth_name &&
54                                  ($tasklist->get('owner') == $auth_name));
55                        if (($my && $owner) || (!$my && !$owner)) {
56                            $result->calendars['tasklists']['tasks/' . $id] =
57                                $all_external_calendars['tasks/' . $id]->toHash();
58                        }
59                    }
60                }
61            }
62        }
63
64        // Resources
65        if (!empty($GLOBALS['conf']['resource']['driver'])) {
66            foreach (Kronolith::getDriver('Resource')->listResources() as $resource) {
67                if ($resource->get('type') != Kronolith_Resource::TYPE_GROUP) {
68                    $rcal = new Kronolith_Calendar_Resource(array(
69                        'resource' => $resource
70                    ));
71                    $result->calendars['resource'][$resource->get('calendar')] = $rcal->toHash();
72                } else {
73                    $rcal = new Kronolith_Calendar_ResourceGroup(array(
74                        'resource' => $resource
75                    ));
76                    $result->calendars['resourcegroup'][$resource->getId()] = $rcal->toHash();
77                }
78            }
79        }
80
81        // Timeobjects
82        foreach ($all_external_calendars as $id => $calendar) {
83            if ($calendar->api() != 'tasks' && $calendar->display()) {
84                $result->calendars['external'][$id] = $calendar->toHash();
85            }
86        }
87
88        // Remote calendars
89        foreach ($GLOBALS['calendar_manager']->get(Kronolith::ALL_REMOTE_CALENDARS) as $url => $calendar) {
90            $result->calendars['remote'][$url] = $calendar->toHash();
91        }
92
93        // Holidays
94        foreach ($GLOBALS['calendar_manager']->get(Kronolith::ALL_HOLIDAYS) as $id => $calendar) {
95            $result->calendars['holiday'][$id] = $calendar->toHash();
96        }
97
98        return $result;
99    }
100
101    /**
102     * TODO
103     */
104    public function listEvents()
105    {
106        global $session;
107
108        $start = new Horde_Date($this->vars->start);
109        $end   = new Horde_Date($this->vars->end);
110        $result = $this->_signedResponse($this->vars->cal);
111        if (!($kronolith_driver = $this->_getDriver($this->vars->cal))) {
112            return $result;
113        }
114        try {
115            $session->close();
116            $events = $kronolith_driver->listEvents($start, $end, array(
117                'show_recurrence' => true,
118                'json' => true)
119            );
120            $session->start();
121            if (count($events)) {
122                $result->events = $events;
123            }
124        } catch (Exception $e) {
125            $session->start();
126            $GLOBALS['notification']->push($e, 'horde.error');
127        }
128        return $result;
129    }
130
131    /**
132     * Returns a JSON object representing the requested event.
133     *
134     * Request variables used:
135     *  - cal:  The calendar id
136     *  - id:   The event id
137     *  - date: The date of the event we are requesting [OPTIONAL]
138     *  - rsd:  The event start date of the instance of a recurring event, if
139     *          requesting a specific instance.
140     *  - red:  The event end date of the instance of a recurring event, if
141     *          requesting a specific instance.
142     */
143    public function getEvent()
144    {
145        $result = new stdClass;
146
147        if (!($kronolith_driver = $this->_getDriver($this->vars->cal)) ||
148            !isset($this->vars->id)) {
149            return $result;
150        }
151
152        try {
153            $event = $kronolith_driver->getEvent($this->vars->id, $this->vars->date);
154            $event->setTimezone(true);
155            $result->event = $event->toJson(null, true, $GLOBALS['prefs']->getValue('twentyFour') ? 'H:i' : 'h:i A');
156            // If recurring, we need to format the dates of this instance, since
157            // Kronolith_Driver#getEvent will return the start/end dates of the
158            // original event in the series.
159            if ($event->recurs() && $this->vars->rsd) {
160                $rs = new Horde_Date($this->vars->rsd);
161                $result->event->rsd = $rs->strftime('%x');
162                $re = new Horde_Date($this->vars->red);
163                $result->event->red = $re->strftime('%x');
164            }
165        } catch (Horde_Exception_NotFound $e) {
166            $GLOBALS['notification']->push(_("The requested event was not found."), 'horde.error');
167        } catch (Exception $e) {
168            $GLOBALS['notification']->push($e, 'horde.error');
169        }
170
171        return $result;
172    }
173
174    /**
175     * Save a new or update an existing event from the AJAX event detail view.
176     *
177     * Request parameters used:
178     * - event:          The event id.
179     * - cal:            The calendar id.
180     * - targetcalendar: If moving events, the targetcalendar to move to.
181     * - as_new:         Save an existing event as a new event.
182     * - recur_edit:     If editing an instance of a recurring event series,
183     *                   how to apply the edit [current|future|all].
184     * - rstart:         If editing an instance of a recurring event series,
185     *                   the original start datetime of this instance.
186     * - rend:           If editing an instance of a recurring event series,
187     *                   the original ending datetime of this instance.
188     * - sendupdates:    Should updates be sent to attendees?
189     * - cstart:         Start time of the client cache.
190     * - cend:           End time of the client cache.
191     */
192    public function saveEvent()
193    {
194        global $injector, $notification, $registry;
195
196        $result = $this->_signedResponse($this->vars->targetcalendar);
197
198        if (!($kronolith_driver = $this->_getDriver($this->vars->targetcalendar))) {
199            return $result;
200        }
201
202        if ($this->vars->as_new) {
203            unset($this->vars->event);
204        }
205        if (!$this->vars->event) {
206            $perms = $injector->getInstance('Horde_Core_Perms');
207            if ($perms->hasAppPermission('max_events') !== true &&
208                $perms->hasAppPermission('max_events') <= Kronolith::countEvents()) {
209                Horde::permissionDeniedError(
210                    'kronolith',
211                    'max_events',
212                    sprintf(
213                        _("You are not allowed to create more than %d events."),
214                        $perms->hasAppPermission('max_events')
215                    )
216                );
217                return $result;
218            }
219        }
220
221        if ($this->vars->event &&
222            $this->vars->cal &&
223            $this->vars->cal != $this->vars->targetcalendar) {
224            if (strpos($kronolith_driver->calendar, '\\')) {
225                list($target, $user) = explode(
226                    '\\', $kronolith_driver->calendar, 2
227                );
228            } else {
229                $target = $kronolith_driver->calendar;
230                $user = $registry->getAuth();
231            }
232            $kronolith_driver = $this->_getDriver($this->vars->cal);
233            // Only delete the event from the source calendar if this user has
234            // permissions to do so.
235            try {
236                $sourceShare = Kronolith::getInternalCalendar(
237                    $kronolith_driver->calendar
238                );
239                $share = Kronolith::getInternalCalendar($target);
240                if ($sourceShare->hasPermission($registry->getAuth(), Horde_Perms::DELETE) &&
241                    (($user == $registry->getAuth() &&
242                      $share->hasPermission($registry->getAuth(), Horde_Perms::EDIT)) ||
243                     ($user != $registry->getAuth() &&
244                      $share->hasPermission($registry->getAuth(), Kronolith::PERMS_DELEGATE)))) {
245                    $kronolith_driver->move($this->vars->event, $target);
246                    $kronolith_driver = $this->_getDriver($this->vars->targetcalendar);
247                }
248            } catch (Exception $e) {
249                $notification->push(
250                    sprintf(
251                        _("There was an error moving the event: %s"),
252                        $e->getMessage()
253                    ),
254                    'horde.error'
255                );
256                return $result;
257            }
258        }
259
260        if ($this->vars->as_new) {
261            $event = $kronolith_driver->getEvent();
262        } else {
263            try {
264                $event = $kronolith_driver->getEvent($this->vars->event);
265            } catch (Horde_Exception_NotFound $e) {
266                $notification->push(
267                    _("The requested event was not found."),
268                    'horde.error'
269                );
270                return $result;
271            } catch (Exception $e) {
272                $notification->push($e);
273                return $result;
274            }
275        }
276
277        if (!$event->hasPermission(Horde_Perms::EDIT)) {
278            $notification->push(
279                _("You do not have permission to edit this event."),
280                'horde.warning'
281            );
282            return $result;
283        }
284
285        $removed_attendees = $old_attendees = array();
286        if ($this->vars->recur_edit && $this->vars->recur_edit != 'all') {
287            switch ($this->vars->recur_edit) {
288            case 'current':
289                $attributes = new stdClass();
290                $attributes->rstart = $this->vars->rstart;
291                $attributes->rend = $this->vars->rend;
292                $this->_addException($event, $attributes);
293
294                // Create a copy of the original event so we can read in the
295                // new form values for the exception. We also MUST reset the
296                // recurrence property even though we won't be using it, since
297                // clone() does not do a deep copy. Otherwise, the original
298                // event's recurrence will become corrupt.
299                $newEvent = clone($event);
300                $newEvent->recurrence = new Horde_Date_Recurrence($event->start);
301                $newEvent->readForm($event);
302
303                // Create an exception event from the new properties.
304                $exception = $this->_copyEvent($event, $newEvent, $attributes);
305                $exception->start = $newEvent->start;
306                $exception->end = $newEvent->end;
307
308                // Save the new exception.
309                $attributes->cstart = $this->vars->cstart;
310                $attributes->cend = $this->vars->cend;
311                $result = $this->_saveEvent(
312                    $exception,
313                    $event,
314                    $attributes);
315                break;
316            case 'future':
317                $instance = new Horde_Date($this->vars->rstart, $event->timezone);
318                $exception = clone($instance);
319                $exception->mday--;
320                if ($event->end->compareDate($exception) > 0) {
321                    // Same as 'all' since this is the first recurrence.
322                    $this->vars->recur_edit = 'all';
323                    return $this->saveEvent();
324                } else {
325                    $event->recurrence->setRecurEnd($exception);
326                    $newEvent = $kronolith_driver->getEvent();
327                    $newEvent->readForm();
328                    $newEvent->uid = null;
329                    $result = $this->_saveEvent(
330                        $newEvent, $event, $this->vars, true
331                    );
332                }
333
334            }
335        } else {
336            try {
337                $old_attendees = $event->attendees;
338                $event->readForm();
339                $removed_attendees = array_diff(
340                    array_keys($old_attendees),
341                    array_keys($event->attendees)
342                );
343                $result = $this->_saveEvent($event);
344            } catch (Exception $e) {
345                $notification->push($e);
346                return $result;
347            }
348        }
349
350        if (($result !== true) && $this->vars->sendupdates) {
351            $type = $event->status == Kronolith::STATUS_CANCELLED
352                ? Kronolith::ITIP_CANCEL
353                : Kronolith::ITIP_REQUEST;
354            Kronolith::sendITipNotifications($event, $notification, $type);
355        }
356
357        // Send a CANCEL iTip for attendees that have been removed, but only if
358        // the entire event isn't being marked as cancelled (which would be
359        // caught above).
360        if (!empty($removed_attendees)) {
361            $to_cancel = array();
362            foreach ($removed_attendees as $email) {
363                $to_cancel[$email] = $old_attendees[$email];
364            }
365            $cancelEvent = clone $event;
366            Kronolith::sendITipNotifications(
367                $cancelEvent, $notification, Kronolith::ITIP_CANCEL, null, null, $to_cancel
368            );
369        }
370        Kronolith::notifyOfResourceRejection($event);
371
372        return $result;
373    }
374
375    /**
376     * TODO
377     */
378    public function quickSaveEvent()
379    {
380        $cal = explode('|', $this->vars->cal, 2);
381        try {
382            $event = Kronolith::quickAdd($this->vars->text, $cal[1]);
383            return $this->_saveEvent($event);
384        } catch (Horde_Exception $e) {
385            $GLOBALS['notification']->push($e);
386            $result = $this->_signedResponse($this->vars->cal);
387            $result->error = true;
388            return $result;
389        }
390    }
391
392    /**
393     * Update event details as a result of a drag/drop operation (which would
394     * only affect the event's start/end times).
395     *
396     * Uses the following request variables:
397     *<pre>
398     *   -cal:  The calendar id.
399     *   -id:   The event id.
400     *   -att:  Attribute hash of changed values. Can contain:
401     *      -start:     A new start datetime for the event.
402     *      -end:       A new end datetime for the event.
403     *      -offDays:   An offset of days to apply to the event.
404     *      -offMins:   An offset of minutes to apply to the event.
405     *      -rstart:    The orginal start datetime of a series instance.
406     *      -rend:      The original end datetime of a series instance.
407     *      -rday:      A new start value for a series instance (used when
408     *                  dragging on the month view where only the date can
409     *                  change, and not the start/end times).
410     *      -u:         Send update to attendees.
411     *</pre>
412     */
413    public function updateEvent()
414    {
415        $result = $this->_signedResponse($this->vars->cal);
416
417        if (!($kronolith_driver = $this->_getDriver($this->vars->cal)) ||
418            !isset($this->vars->id)) {
419            return $result;
420        }
421
422        try {
423            $oevent = $kronolith_driver->getEvent($this->vars->id);
424        } catch (Exception $e) {
425            $GLOBALS['notification']->push($e, 'horde.error');
426            return $result;
427        }
428        if (!$oevent) {
429            $GLOBALS['notification']->push(_("The requested event was not found."), 'horde.error');
430            return $result;
431        } elseif (!$oevent->hasPermission(Horde_Perms::EDIT)) {
432            $GLOBALS['notification']->push(_("You do not have permission to edit this event."), 'horde.warning');
433            return $result;
434        }
435
436        $attributes = Horde_Serialize::unserialize($this->vars->att, Horde_Serialize::JSON);
437
438        // If this is a recurring event, need to create an exception.
439        if ($oevent->recurs()) {
440            $this->_addException($oevent, $attributes);
441            $event = $this->_copyEvent($oevent, null, $attributes);
442        } else {
443            $event = clone($oevent);
444        }
445
446        foreach ($attributes as $attribute => $value) {
447            switch ($attribute) {
448            case 'start':
449                $newDate = new Horde_Date($value);
450                $newDate->setTimezone($event->start->timezone);
451                $event->start = clone($newDate);
452                break;
453
454            case 'end':
455                $newDate = new Horde_Date($value);
456                $newDate->setTimezone($event->end->timezone);
457                $event->end = clone($newDate);
458                if ($event->end->hour == 23 &&
459                    $event->end->min == 59 &&
460                    $event->end->sec == 59) {
461                    $event->end->mday++;
462                    $event->end->hour = $event->end->min = $event->end->sec = 0;
463                }
464                break;
465
466            case 'offDays':
467                $event->start->mday += $value;
468                $event->end->mday += $value;
469                break;
470
471            case 'offMins':
472                $event->start->min += $value;
473                $event->end->min += $value;
474                break;
475            }
476        }
477
478        $result = $this->_saveEvent($event, ($oevent->recurs() ? $oevent : null), $attributes);
479        if ($this->vars->u) {
480            Kronolith::sendITipNotifications($event, $GLOBALS['notification'], Kronolith::ITIP_REQUEST);
481        }
482
483        return $result;
484    }
485
486    /**
487     * Deletes an event, or an instance of an event series from the backend.
488     *
489     * Uses the following request variables:
490     *<pre>
491     *   - cal:          The calendar id.
492     *   - id:           The event id.
493     *   - r:            If this is an event series, what type of deletion to
494     *                   perform [future | current | all].
495     *   - rstart:       The start time of the event instance being removed, if
496     *                   this is a series instance.
497     *   - cstart:       The start date of the client event cache.
498     *   - cend:         The end date of the client event cache.
499     *   - sendupdates:  Send cancellation notice to attendees?
500     * </pre>
501     */
502    public function deleteEvent()
503    {
504        $result = new stdClass;
505        $instance = null;
506
507        if (!($kronolith_driver = $this->_getDriver($this->vars->cal)) ||
508            !isset($this->vars->id)) {
509            return $result;
510        }
511
512        try {
513            $event = $kronolith_driver->getEvent($this->vars->id);
514            if (!$event->hasPermission(Horde_Perms::DELETE)) {
515                $GLOBALS['notification']->push(_("You do not have permission to delete this event."), 'horde.warning');
516                return $result;
517            }
518            $range = null;
519            if ($event->recurs() && $this->vars->r != 'all') {
520                switch ($this->vars->r) {
521                case 'future':
522                    // Deleting all future instances.
523                    // @TODO: Check if we need to find future exceptions
524                    //        that are after $recurEnd and remove those as well.
525                    $instance = new Horde_Date($this->vars->rstart, $event->timezone);
526                    $recurEnd = clone($instance);
527                    $recurEnd->hour = 0;
528                    $recurEnd->min = 0;
529                    $recurEnd->sec = 0;
530                    $recurEnd->mday--;
531                    if ($event->end->compareDate($recurEnd) > 0) {
532                        $kronolith_driver->deleteEvent($event->id);
533                        $result = $this->_signedResponse($this->vars->cal);
534                        $result->events = array();
535                    } else {
536                        $event->recurrence->setRecurEnd($recurEnd);
537                        $result = $this->_saveEvent($event, $event, $this->vars);
538                    }
539                    $range = Kronolith::RANGE_THISANDFUTURE;
540                    break;
541                case 'current':
542                    // Deleting only the current instance.
543                    $instance = new Horde_Date($this->vars->rstart, $event->timezone);
544                    $event->recurrence->addException(
545                        $instance->year, $instance->month, $instance->mday);
546                    $result = $this->_saveEvent($event, $event, $this->vars);
547                }
548            } else {
549                // Deleting an entire series, or this is a single event only.
550                $kronolith_driver->deleteEvent($event->id);
551                $result = $this->_signedResponse($this->vars->cal);
552                $result->events = array();
553                $result->uid = $event->uid;
554            }
555
556            if ($this->vars->sendupdates) {
557                Kronolith::sendITipNotifications(
558                    $event, $GLOBALS['notification'], Kronolith::ITIP_CANCEL, $instance, $range);
559            }
560            $result->deleted = true;
561        } catch (Horde_Exception_NotFound $e) {
562            $GLOBALS['notification']->push(_("The requested event was not found."), 'horde.error');
563        } catch (Exception $e) {
564            $GLOBALS['notification']->push($e, 'horde.error');
565        }
566
567        return $result;
568    }
569
570    /**
571     * TODO
572     */
573    public function searchEvents()
574    {
575        $query = Horde_Serialize::unserialize($this->vars->query, Horde_Serialize::JSON);
576        if (!isset($query->start)) {
577            $query->start = new Horde_Date($_SERVER['REQUEST_TIME']);
578        }
579        if (!isset($query->end)) {
580            $query->end = null;
581        }
582        switch ($this->vars->time) {
583        case 'all':
584            $query->start = null;
585            $query->end = null;
586            break;
587        case 'future':
588            $query->start = new Horde_Date($_SERVER['REQUEST_TIME']);
589            $query->end = null;
590            break;
591        case 'past':
592            $query->start = null;
593            $query->end = new Horde_Date($_SERVER['REQUEST_TIME']);
594            break;
595        }
596
597        $tagger = new Kronolith_Tagger();
598        $cals = Horde_Serialize::unserialize($this->vars->cals, Horde_Serialize::JSON);
599        $events = array();
600        foreach ($cals as $cal) {
601            if (!($kronolith_driver = $this->_getDriver($cal))) {
602                continue;
603            }
604            try {
605                $result = $kronolith_driver->search($query, true);
606                if ($result) {
607                    $events[$cal] = $result;
608                }
609            } catch (Exception $e) {
610                $GLOBALS['notification']->push($e, 'horde.error');
611            }
612            $split = explode('|', $cal);
613            if ($split[0] == 'internal') {
614                $result = $tagger->search($query->title, array('type' => 'event', 'calendar' => $split[1]));
615                foreach ($result['events'] as $uid) {
616                    Kronolith::addSearchEvents($events[$cal], $kronolith_driver->getByUID($uid), $query, true);
617                }
618            }
619        }
620
621        $result = new stdClass;
622        $result->view = 'search';
623        $result->query = $this->vars->query;
624        if ($events) {
625            $result->events = $events;
626        }
627
628        return $result;
629    }
630
631    /**
632     * TODO
633     */
634    public function listTasks()
635    {
636        if (!$GLOBALS['registry']->hasMethod('tasks/listTasks')) {
637            return false;
638        }
639
640        $result = new stdClass;
641        $result->list = $this->vars->list;
642        $result->type = $this->vars->type;
643        try {
644            $tasks = $GLOBALS['registry']->tasks
645                ->listTasks(array(
646                    'tasklists' => $this->vars->list,
647                    'completed' => $this->vars->type == 'incomplete' ? 'future_incomplete' : $this->vars->type,
648                    'include_tags' => true,
649                    'external' => false,
650                    'json' => true
651                ));
652            if (count($tasks)) {
653                $result->tasks = $tasks;
654            }
655        } catch (Exception $e) {
656            $GLOBALS['notification']->push($e, 'horde.error');
657        }
658
659        return $result;
660    }
661
662    /**
663     * TODO
664     */
665    public function getTask()
666    {
667        if (!$GLOBALS['registry']->hasMethod('tasks/getTask') ||
668            !isset($this->vars->id) ||
669            !isset($this->vars->list)) {
670            return false;
671        }
672
673        $result = new stdClass;
674        try {
675            $task = $GLOBALS['registry']->tasks->getTask($this->vars->list, $this->vars->id);
676            if ($task) {
677                $result->task = $task->toJson(true, $GLOBALS['prefs']->getValue('twentyFour') ? 'H:i' : 'h:i A');
678            } else {
679                $GLOBALS['notification']->push(_("The requested task was not found."), 'horde.error');
680            }
681        } catch (Exception $e) {
682            $GLOBALS['notification']->push($e, 'horde.error');
683        }
684
685        return $result;
686    }
687
688    /**
689     * TODO
690     */
691    public function saveTask()
692    {
693        if (!$GLOBALS['registry']->hasMethod('tasks/updateTask') ||
694            !$GLOBALS['registry']->hasMethod('tasks/addTask')) {
695            return false;
696        }
697
698        $id = $this->vars->task_id;
699        $list = $this->vars->old_tasklist;
700        $task = $this->vars->task;
701        $result = $this->_signedResponse('tasklists|tasks/' . $task['tasklist']);
702
703        $due = trim($task['due_date'] . ' ' . $task['due_time']);
704        if (!empty($due)) {
705            try {
706                $due = Kronolith::parseDate($due);
707                $task['due'] = $due->timestamp();
708            } catch (Exception $e) {
709                $GLOBALS['notification']->push($e, 'horde.error');
710                return $result;
711            }
712        }
713
714        if ($task['alarm']['on']) {
715            $value = $task['alarm']['value'];
716            $unit = $task['alarm']['unit'];
717            if ($value == 0) {
718                $value = $unit = 1;
719            }
720            $task['alarm'] = $value * $unit;
721            if (isset($task['alarm_methods']) && isset($task['methods'])) {
722                foreach (array_keys($task['methods']) as $method) {
723                    if (!in_array($method, $task['alarm_methods'])) {
724                        unset($task['methods'][$method]);
725                    }
726                }
727                foreach ($task['alarm_methods'] as $method) {
728                    if (!isset($task['methods'][$method])) {
729                        $task['methods'][$method] = array();
730                    }
731                }
732            } else {
733                $task['methods'] = array();
734            }
735        } else {
736            $task['alarm'] = 0;
737            $task['methods'] = array();
738        }
739        unset($task['alarm_methods']);
740
741        if (!isset($task['completed'])) {
742            $task['completed'] = false;
743        }
744
745        if ($this->vars->recur && !empty($due)) {
746            $task['recurrence'] = Kronolith_Event::readRecurrenceForm($due, 'UTC');
747        }
748
749        $task['tags'] = Horde_Util::getFormData('tags');
750
751        try {
752            $ids = ($id && $list)
753                ? $GLOBALS['registry']->tasks->updateTask($list, $id, $task)
754                : $GLOBALS['registry']->tasks->addTask($task);
755            if (!$id) {
756                $id = $ids[0];
757            }
758            $task = $GLOBALS['registry']->tasks->getTask($task['tasklist'], $id);
759            $result->tasks = array($id => $task->toJson(false, $GLOBALS['prefs']->getValue('twentyFour') ? 'H:i' : 'h:i A'));
760            $result->type = $task->completed ? 'complete' : 'incomplete';
761            $result->list = $task->tasklist;
762        } catch (Exception $e) {
763            $GLOBALS['notification']->push($e, 'horde.error');
764            return $result;
765        }
766
767        if ($due &&
768            $kronolith_driver = $this->_getDriver('tasklists|tasks/' . $task->tasklist)) {
769            try {
770                $event = $kronolith_driver->getEvent('_tasks' . $id);
771                $end = clone $due;
772                $end->hour = 23;
773                $end->min = $end->sec = 59;
774                $start = clone $due;
775                $start->hour = $start->min = $start->sec = 0;
776                $events = array();
777                Kronolith::addEvents($events, $event, $start, $end, true, true);
778                if (count($events)) {
779                    $result->events = $events;
780                }
781            } catch (Horde_Exception_NotFound $e) {
782            } catch (Exception $e) {
783                $GLOBALS['notification']->push($e, 'horde.error');
784            }
785        }
786
787        return $result;
788    }
789
790    /**
791     * TODO
792     */
793    public function quickSaveTask()
794    {
795        if (!$GLOBALS['registry']->hasMethod('tasks/quickAdd')) {
796            return false;
797        }
798
799        $result = $this->_signedResponse(
800            'tasklists|tasks/' . $this->vars->tasklist);
801
802        try {
803            $ids = $GLOBALS['registry']->tasks->quickAdd($this->vars->text);
804            $result->type = 'incomplete';
805            $result->list = $this->vars->tasklist;
806            $result->tasks = array();
807            foreach ($ids as $uid) {
808                $task = $GLOBALS['registry']->tasks->export($uid, 'raw');
809                $result->tasks[$task->id] = $task->toJson(
810                    false,
811                    $GLOBALS['prefs']->getValue('twentyFour') ? 'H:i' : 'h:i A'
812                );
813            }
814        } catch (Exception $e) {
815            $GLOBALS['notification']->push($e, 'horde.error');
816        }
817
818        return $result;
819    }
820
821    /**
822     * TODO
823     */
824    public function deleteTask()
825    {
826        $result = new stdClass;
827
828        if (!$GLOBALS['registry']->hasMethod('tasks/deleteTask') ||
829            !isset($this->vars->id) ||
830            !isset($this->vars->list)) {
831            return $result;
832        }
833
834        try {
835            $GLOBALS['registry']->tasks->deleteTask($this->vars->list, $this->vars->id);
836            $result->deleted = true;
837        } catch (Exception $e) {
838            $GLOBALS['notification']->push($e, 'horde.error');
839        }
840
841        return $result;
842    }
843
844    /**
845     * TODO
846     */
847    public function toggleCompletion()
848    {
849        $result = new stdClass;
850
851        if (!$GLOBALS['registry']->hasMethod('tasks/toggleCompletion')) {
852            return $result;
853        }
854
855        try {
856            $result->toggled = $GLOBALS['registry']->tasks->toggleCompletion($this->vars->id, $this->vars->list);
857        } catch (Exception $e) {
858            $GLOBALS['notification']->push($e, 'horde.error');
859        }
860
861        return $result;
862    }
863
864    /**
865     * Generate a list of most frequently used tags for the current user.
866     */
867    public function listTopTags()
868    {
869        $tagger = new Kronolith_Tagger();
870        $result = new stdClass;
871        $result->tags = array();
872        $tags = $tagger->getCloud($GLOBALS['registry']->getAuth(), 10, true);
873        foreach ($tags as $tag) {
874            $result->tags[] = $tag['tag_name'];
875        }
876        return $result;
877    }
878
879    /**
880     * Return fb information for the requested attendee or resource.
881     *
882     * Uses the following request parameters:
883     *<pre>
884     *  -email:    The attendee's email address.
885     *  -resource: The resource id.
886     *</pre>
887     */
888    public function getFreeBusy()
889    {
890        $result = new stdClass;
891        if ($this->vars->email) {
892            try {
893                $result->fb = Kronolith_FreeBusy::get($this->vars->email, true);
894            } catch (Exception $e) {
895                $GLOBALS['notification']->push($e->getMessage(), 'horde.warning');
896            }
897        } elseif ($this->vars->resource) {
898            try {
899                $resource = Kronolith::getDriver('Resource')
900                    ->getResource($this->vars->resource);
901                try {
902                    $result->fb = $resource->getFreeBusy(null, null, true, true);
903                } catch (Horde_Exception $e) {
904                    // Resource groups can't provide FB information.
905                    $result->fb = null;
906                }
907            } catch (Exception $e) {
908                $GLOBALS['notification']->push($e->getMessage(), 'horde.warning');
909            }
910        }
911
912        return $result;
913    }
914
915    /**
916     * TODO
917     */
918    public function searchCalendars()
919    {
920        $result = new stdClass;
921        $result->events = 'Searched for calendars: ' . $this->vars->title;
922        return $result;
923    }
924
925    /**
926     * TODO
927     */
928    public function saveCalendar()
929    {
930        $calendar_id = $this->vars->calendar;
931        $result = new stdClass;
932
933        switch ($this->vars->type) {
934        case 'internal':
935            $info = array();
936            foreach (array('name', 'color', 'description', 'tags') as $key) {
937                $info[$key] = $this->vars->$key;
938            }
939
940            // Create a calendar.
941            if (!$calendar_id) {
942                if (!$GLOBALS['registry']->getAuth() ||
943                    $GLOBALS['prefs']->isLocked('default_share')) {
944                    return $result;
945                }
946                try {
947                    $calendar = Kronolith::addShare($info);
948                    Kronolith::readPermsForm($calendar);
949                    if ($calendar->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::SHOW)) {
950                        $wrapper = new Kronolith_Calendar_Internal(array('share' => $calendar));
951                        $result->saved = true;
952                        $result->id = $calendar->getName();
953                        $result->calendar = $wrapper->toHash();
954                    }
955                } catch (Exception $e) {
956                    $GLOBALS['notification']->push($e, 'horde.error');
957                    return $result;
958                }
959                $GLOBALS['notification']->push(sprintf(_("The calendar \"%s\" has been created."), $info['name']), 'horde.success');
960                break;
961            }
962
963            // Update a calendar.
964            try {
965                $calendar = $GLOBALS['injector']->getInstance('Kronolith_Shares')->getShare($calendar_id);
966                $original_name = $calendar->get('name');
967                $original_owner = $calendar->get('owner');
968                Kronolith::updateShare($calendar, $info);
969                Kronolith::readPermsForm($calendar);
970                if ($calendar->get('owner') != $original_owner) {
971                    $result->deleted = true;
972                }
973                if ($calendar->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::SHOW)) {
974                    $wrapper = new Kronolith_Calendar_Internal(array('share' => $calendar));
975                    $result->saved = true;
976                    $result->calendar = $wrapper->toHash();
977                }
978            } catch (Exception $e) {
979                $GLOBALS['notification']->push($e, 'horde.error');
980                return $result;
981
982            }
983            if ($calendar->get('name') != $original_name) {
984                $GLOBALS['notification']->push(sprintf(_("The calendar \"%s\" has been renamed to \"%s\"."), $original_name, $calendar->get('name')), 'horde.success');
985            } else {
986                $GLOBALS['notification']->push(sprintf(_("The calendar \"%s\" has been saved."), $original_name), 'horde.success');
987            }
988            break;
989
990        case 'tasklists':
991            $calendar = array();
992            foreach (array('name', 'color', 'description') as $key) {
993                $calendar[$key] = $this->vars->$key;
994            }
995
996            // Create a task list.
997            if (!$calendar_id) {
998                if (!$GLOBALS['registry']->getAuth() ||
999                    $GLOBALS['prefs']->isLocked('default_share')) {
1000                    return $result;
1001                }
1002                try {
1003                    $tasklistId = $GLOBALS['registry']->tasks->addTasklist($calendar['name'], $calendar['description'], $calendar['color']);
1004                    $tasklists = $GLOBALS['registry']->tasks->listTasklists(true);
1005                    if (!isset($tasklists[$tasklistId])) {
1006                        $GLOBALS['notification']->push(_("Added task list not found."), 'horde.error');
1007                        return $result;
1008                    }
1009                    $tasklist = $tasklists[$tasklistId];
1010                    Kronolith::readPermsForm($tasklist);
1011                    if ($tasklist->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::SHOW)) {
1012                        $wrapper = new Kronolith_Calendar_External_Tasks(array('api' => 'tasks', 'name' => $tasklistId, 'share' => $tasklist));
1013
1014                        // Update external calendars caches.
1015                        $all_external = $GLOBALS['session']->get('kronolith', 'all_external_calendars');
1016                        $all_external[] = array('a' => 'tasks', 'n' => $tasklistId, 'd' => $tasklist->get('name'));
1017                        $GLOBALS['session']->set('kronolith', 'all_external_calendars', $all_external);
1018                        $display_external = $GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_EXTERNAL_CALENDARS);
1019                        $display_external[] = 'tasks/' . $tasklistId;
1020                        $GLOBALS['calendar_manager']->set(Kronolith::DISPLAY_EXTERNAL_CALENDARS, $display_external);
1021                        $GLOBALS['prefs']->setValue('display_external_cals', serialize($display_external));
1022                        $all_external = $GLOBALS['calendar_manager']->get(Kronolith::ALL_EXTERNAL_CALENDARS);
1023                        $all_external['tasks/' . $tasklistId] = $wrapper;
1024                        $GLOBALS['calendar_manager']->set(Kronolith::ALL_EXTERNAL_CALENDARS, $all_external);
1025
1026                        $result->saved = true;
1027                        $result->id = 'tasks/' . $tasklistId;
1028                        $result->calendar = $wrapper->toHash();
1029                    }
1030                } catch (Exception $e) {
1031                    $GLOBALS['notification']->push($e, 'horde.error');
1032                    return $result;
1033                }
1034                $GLOBALS['notification']->push(sprintf(_("The task list \"%s\" has been created."), $calendar['name']), 'horde.success');
1035                break;
1036            }
1037
1038            // Update a task list.
1039            $calendar_id = substr($calendar_id, 6);
1040            try {
1041                $GLOBALS['registry']->tasks->updateTasklist($calendar_id, $calendar);
1042                $tasklists = $GLOBALS['registry']->tasks->listTasklists(true, Horde_Perms::EDIT);
1043                $tasklist = $tasklists[$calendar_id];
1044                $original_owner = $tasklist->get('owner');
1045                Kronolith::readPermsForm($tasklist);
1046                if ($tasklist->get('owner') != $original_owner) {
1047                    $result->deleted = true;
1048                }
1049                if ($tasklist->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::SHOW)) {
1050                    $wrapper = new Kronolith_Calendar_External_Tasks(array('api' => 'tasks', 'name' => $calendar_id, 'share' => $tasklist));
1051                    $result->saved = true;
1052                    $result->calendar = $wrapper->toHash();
1053                }
1054            } catch (Exception $e) {
1055                $GLOBALS['notification']->push($e, 'horde.error');
1056                return $result;
1057            }
1058            if ($tasklist->get('name') != $calendar['name']) {
1059                $GLOBALS['notification']->push(sprintf(_("The task list \"%s\" has been renamed to \"%s\"."), $tasklist->get('name'), $calendar['name']), 'horde.success');
1060            } else {
1061                $GLOBALS['notification']->push(sprintf(_("The task list \"%s\" has been saved."), $tasklist->get('name')), 'horde.success');
1062            }
1063            break;
1064
1065        case 'remote':
1066            $calendar = array();
1067            foreach (array('name', 'desc', 'url', 'color', 'user', 'password') as $key) {
1068                $calendar[$key] = $this->vars->$key;
1069            }
1070            try {
1071                Kronolith::subscribeRemoteCalendar($calendar, $calendar_id);
1072            } catch (Exception $e) {
1073                $GLOBALS['notification']->push($e, 'horde.error');
1074                return $result;
1075            }
1076            if ($calendar_id) {
1077                $GLOBALS['notification']->push(sprintf(_("The calendar \"%s\" has been saved."), $calendar['name']), 'horde.success');
1078            } else {
1079                $GLOBALS['notification']->push(sprintf(_("You have been subscribed to \"%s\" (%s)."), $calendar['name'], $calendar['url']), 'horde.success');
1080                $result->id = $calendar['url'];
1081            }
1082            $wrapper = new Kronolith_Calendar_Remote($calendar);
1083            $result->saved = true;
1084            $result->calendar = $wrapper->toHash();
1085            break;
1086
1087        case 'resource':
1088            foreach (array('name', 'description', 'response_type') as $key) {
1089                $info[$key] = $this->vars->$key;
1090            }
1091
1092            if (!$calendar_id) {
1093                // New resource
1094                // @TODO: Groups.
1095                if (!$GLOBALS['registry']->isAdmin() &&
1096                    !$GLOBALS['injector']->getInstance('Horde_Core_Perms')->hasAppPermission('resource_management')) {
1097                    $GLOBALS['notification']->push(_("You are not allowed to create new resources."), 'horde.error');
1098                    return $result;
1099                }
1100                $resource = Kronolith_Resource::addResource(new Kronolith_Resource_Single($info));
1101            } else {
1102                try {
1103                    $rdriver = Kronolith::getDriver('Resource');
1104                    $resource = $rdriver->getResource($rdriver->getResourceIdByCalendar($calendar_id));
1105                    if (!($resource->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::EDIT))) {
1106                        $GLOBALS['notification']->push(_("You are not allowed to edit this resource."), 'horde.error');
1107                        return $result;
1108                    }
1109                    foreach (array('name', 'description', 'response_type', 'email') as $key) {
1110                        $resource->set($key, $this->vars->$key);
1111                    }
1112                    $resource->save();
1113                } catch (Kronolith_Exception $e) {
1114                    $GLOBALS['notification']->push($e->getMessage(), 'horde.error');
1115                    return $result;
1116                }
1117            }
1118            $wrapper = new Kronolith_Calendar_Resource(array('resource' => $resource));
1119            $result->calendar = $wrapper->toHash();
1120            $result->saved = true;
1121            $result->id = $resource->get('calendar');
1122            $GLOBALS['notification']->push(sprintf(_("The resource \"%s\" has been saved."), $resource->get('name'), 'horde.success'));
1123            break;
1124
1125        case 'resourcegroup':
1126            if (empty($calendar_id)) {
1127                // New resource group.
1128                $resource = Kronolith_Resource::addResource(
1129                    new Kronolith_Resource_Group(array(
1130                        'name' => $this->vars->name,
1131                        'description' => $this->vars->description,
1132                        'members' => $this->vars->members)
1133                    )
1134                );
1135            } else {
1136                $driver = Kronolith::getDriver('Resource');
1137                $resource = $driver->getResource($calendar_id);
1138                $resource->set('name', $this->vars->name);
1139                $resource->set('description', $this->vars->description);
1140                $resource->set('members', $this->vars->members);
1141                $resource->save();
1142            }
1143
1144            $wrapper = new Kronolith_Calendar_ResourceGroup(array('resource' => $resource));
1145            $result->calendar = $wrapper->toHash();
1146            $result->saved = true;
1147            $result->id = $resource->get('calendar');
1148            $GLOBALS['notification']->push(sprintf(_("The resource group \"%s\" has been saved."), $resource->get('name'), 'horde.success'));
1149            break;
1150        }
1151
1152        return $result;
1153    }
1154
1155    /**
1156     * TODO
1157     */
1158    public function deleteCalendar()
1159    {
1160        $calendar_id = $this->vars->calendar;
1161        $result = new stdClass;
1162
1163        switch ($this->vars->type) {
1164        case 'internal':
1165            try {
1166                $calendar = $GLOBALS['injector']->getInstance('Kronolith_Shares')->getShare($calendar_id);
1167            } catch (Exception $e) {
1168                $GLOBALS['notification']->push($e, 'horde.error');
1169                return $result;
1170            }
1171            try {
1172                Kronolith::deleteShare($calendar);
1173            } catch (Exception $e) {
1174                $GLOBALS['notification']->push(sprintf(_("Unable to delete \"%s\": %s"), $calendar->get('name'), $e->getMessage()), 'horde.error');
1175                return $result;
1176            }
1177            $GLOBALS['notification']->push(sprintf(_("The calendar \"%s\" has been deleted."), $calendar->get('name')), 'horde.success');
1178            break;
1179
1180        case 'tasklists':
1181            $calendar_id = substr($calendar_id, 6);
1182            $tasklists = $GLOBALS['registry']->tasks->listTasklists(true);
1183            if (!isset($tasklists[$calendar_id])) {
1184                $GLOBALS['notification']->push(_("You are not allowed to delete this task list."), 'horde.error');
1185                return $result;
1186            }
1187            try {
1188                $GLOBALS['registry']->tasks->deleteTasklist($calendar_id);
1189            } catch (Exception $e) {
1190                $GLOBALS['notification']->push(sprintf(_("Unable to delete \"%s\": %s"), $tasklists[$calendar_id]->get('name'), $e->getMessage()), 'horde.error');
1191                return $result;
1192            }
1193            $GLOBALS['notification']->push(sprintf(_("The task list \"%s\" has been deleted."), $tasklists[$calendar_id]->get('name')), 'horde.success');
1194            break;
1195
1196        case 'remote':
1197            try {
1198                $deleted = Kronolith::unsubscribeRemoteCalendar($calendar_id);
1199            } catch (Exception $e) {
1200                $GLOBALS['notification']->push($e, 'horde.error');
1201                return $result;
1202            }
1203            $GLOBALS['notification']->push(sprintf(_("You have been unsubscribed from \"%s\" (%s)."), $deleted['name'], $deleted['url']), 'horde.success');
1204            break;
1205
1206        case 'resource':
1207            try {
1208                $rdriver = Kronolith::getDriver('Resource');
1209                $resource = $rdriver->getResource($rdriver->getResourceIdByCalendar($calendar_id));
1210                if (!($resource->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::DELETE))) {
1211                    $GLOBALS['notification']->push(_("You are not allowed to delete this resource."), 'horde.error');
1212                    return $result;
1213                }
1214                $name = $resource->get('name');
1215                $rdriver->delete($resource);
1216            } catch (Kronolith_Exception $e) {
1217                $GLOBALS['notification']->push($e->getMessage(), 'horde.error');
1218                return $result;
1219            }
1220
1221            $GLOBALS['notification']->push(sprintf(_("The resource \"%s\" has been deleted."), $name), 'horde.success');
1222            break;
1223
1224        case 'resourcegroup':
1225            try {
1226                $rdriver = Kronolith::getDriver('Resource');
1227                $resource = $rdriver->getResource($calendar_id);
1228                if (!($resource->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::DELETE))) {
1229                    $GLOBALS['notification']->push(_("You are not allowed to delete this resource."), 'horde.error');
1230                    return $result;
1231                }
1232                $name = $resource->get('name');
1233                $rdriver->delete($resource);
1234            } catch (Kronolith_Exception $e) {
1235                $GLOBALS['notification']->push($e->getMessage(), 'horde.error');
1236                return $result;
1237            }
1238            $GLOBALS['notification']->push(sprintf(_("The resource \"%s\" has been deleted."), $name), 'horde.success');
1239
1240        }
1241        $result->deleted = true;
1242
1243        return $result;
1244    }
1245
1246    /**
1247     * Returns the information for a shared internal calendar.
1248     */
1249    public function getCalendar()
1250    {
1251        $result = new stdClass;
1252        $all_calendars = $GLOBALS['calendar_manager']->get(Kronolith::ALL_CALENDARS);
1253        if (!isset($all_calendars[$this->vars->cal]) && !$GLOBALS['conf']['share']['hidden']) {
1254                $GLOBALS['notification']->push(_("You are not allowed to view this calendar."), 'horde.error');
1255                return $result;
1256        } elseif (!isset($all_calendars[$this->vars->cal])) {
1257            // Subscribing to a "hidden" share, check perms.
1258            $kronolith_shares = $GLOBALS['injector']->getInstance('Kronolith_Shares');
1259            $share = $kronolith_shares->getShare($this->vars->cal);
1260            if (!$share->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::READ)) {
1261                $GLOBALS['notification']->push(_("You are not allowed to view this calendar."), 'horde.error');
1262                return $result;
1263            }
1264            $calendar = new Kronolith_Calendar_Internal(array('share' => $share));
1265        } else {
1266            $calendar = $all_calendars[$this->vars->cal];
1267        }
1268
1269        $result->calendar = $calendar->toHash();
1270        return $result;
1271    }
1272
1273    /**
1274     * TODO
1275     */
1276    public function getRemoteInfo()
1277    {
1278        $params = array('timeout' => 15);
1279        if ($user = $this->vars->user) {
1280            $params['user'] = $user;
1281            $params['password'] = $this->vars->password;
1282        }
1283        if (!empty($GLOBALS['conf']['http']['proxy']['proxy_host'])) {
1284            $params['proxy'] = $GLOBALS['conf']['http']['proxy'];
1285        }
1286
1287        $result = new stdClass;
1288        try {
1289            $driver = $GLOBALS['injector']->getInstance('Kronolith_Factory_Driver')->create('Ical', $params);
1290            $driver->open($this->vars->url);
1291            if ($driver->isCalDAV()) {
1292                $result->success = true;
1293                // TODO: find out how to retrieve calendar information via CalDAV.
1294            } else {
1295                $ical = $driver->getRemoteCalendar(false);
1296                $result->success = true;
1297                try {
1298                    $name = $ical->getAttribute('X-WR-CALNAME');
1299                    $result->name = $name;
1300                } catch (Horde_Icalendar_Exception $e) {}
1301                try {
1302                    $desc = $ical->getAttribute('X-WR-CALDESC');
1303                    $result->desc = $desc;
1304                } catch (Horde_Icalendar_Exception $e) {}
1305            }
1306        } catch (Exception $e) {
1307            if ($e->getCode() == 401) {
1308                $result->auth = true;
1309            } else {
1310                $GLOBALS['notification']->push($e, 'horde.error');
1311            }
1312        }
1313
1314        return $result;
1315    }
1316
1317    /**
1318     * TODO
1319     */
1320    public function saveCalPref()
1321    {
1322        return false;
1323    }
1324
1325    /**
1326     * Return a list of available resources.
1327     *
1328     * @return array  A hash of resource_id => resource sorted by resource name.
1329     */
1330    public function getResourceList()
1331    {
1332        $data = array();
1333        $resources = Kronolith::getDriver('Resource')
1334            ->listResources(Horde_Perms::READ, array(), 'name');
1335        foreach ($resources as $resource) {
1336            $data[] = $resource->toJson();
1337        }
1338
1339        return $data;
1340    }
1341
1342    /**
1343     * Handle output of the embedded widget: allows embedding calendar widgets
1344     * in external websites.
1345     *
1346     * The following arguments are required:
1347     *   - calendar: The share_name for the requested calendar.
1348     *   - container: The DOM node to populate with the widget.
1349     *   - view: The view (block) we want.
1350     *
1351     * The following are optional (and are not used for all views)
1352     *   - css
1353     *   - days
1354     *   - maxevents: The maximum number of events to show.
1355     *   - months: The number of months to include.
1356     */
1357    public function embed()
1358    {
1359        global $page_output, $registry;
1360
1361        /* First, determine the type of view we are asking for */
1362        $view = $this->vars->view;
1363
1364        /* The DOM container to put the HTML in on the remote site */
1365        $container = $this->vars->container;
1366
1367        /* The share_name of the calendar to display */
1368        $calendar = $this->vars->calendar;
1369
1370        /* Deault to showing only 1 month when we have a choice */
1371        $count_month = $this->vars->get('months', 1);
1372
1373        /* Default to no limit for the number of events */
1374        $max_events = $this->vars->get('maxevents', 0);
1375
1376        /* Default to one week */
1377        $count_days = $this->vars->get('days', 7);
1378
1379        if ($this->vars->css == 'none') {
1380            $nocss = true;
1381        }
1382
1383        /* Build the block parameters */
1384        $params = array(
1385            'calendar' => $calendar,
1386            'maxevents' => $max_events,
1387            'months' => $count_month,
1388            'days' => $count_days
1389        );
1390
1391        /* Call the Horde_Block api to get the calendar HTML */
1392        $title = $registry->call('horde/blockTitle', array('kronolith', $view, $params));
1393        $results = $registry->call('horde/blockContent', array('kronolith', $view, $params));
1394
1395        /* Some needed paths */
1396        $js_path = $registry->get('jsuri', 'kronolith');
1397
1398        /* Local js */
1399        $jsurl = Horde::url($js_path . '/embed.js', true);
1400
1401        /* Horde's js */
1402        $hjs_path = $registry->get('jsuri', 'horde');
1403        $hjsurl = Horde::url($hjs_path . '/tooltips.js', true);
1404        $pturl = Horde::url($hjs_path . '/prototype.js', true);
1405
1406        /* CSS */
1407        if (empty($nocss)) {
1408            $page_output->addThemeStylesheet('embed.css');
1409
1410            Horde::startBuffer();
1411            $page_output->includeStylesheetFiles(array('nobase' => true), true);
1412            $css = Horde::endBuffer();
1413        } else {
1414            $css = '';
1415        }
1416
1417        /* Escape the text and put together the javascript to send back */
1418        $container = Horde_Serialize::serialize($container, Horde_Serialize::JSON);
1419        $results = Horde_Serialize::serialize('<div class="kronolith_embedded"><div class="title">' . $title . '</div>' . $results . '</div>', Horde_Serialize::JSON);
1420
1421        $js = <<<EOT
1422if (typeof kronolith == 'undefined') {
1423    if (typeof Prototype == 'undefined') {
1424        document.write('<script type="text/javascript" src="$pturl"></script>');
1425    }
1426    if (typeof Horde_ToolTips == 'undefined') {
1427        Horde_ToolTips_Autoload = false;
1428        document.write('<script type="text/javascript" src="$hjsurl"></script>');
1429    }
1430    kronolith = new Object();
1431    kronolithNodes = new Array();
1432    document.write('<script type="text/javascript" src="$jsurl"></script>');
1433    document.write('$css');
1434}
1435kronolithNodes[kronolithNodes.length] = $container;
1436kronolith[$container] = $results;
1437EOT;
1438
1439        return new Horde_Core_Ajax_Response_Raw($js, 'text/javascript');
1440    }
1441
1442    public function toTimeslice()
1443    {
1444        $driver = $this->_getDriver($this->vars->cal);
1445        $event = $driver->getEvent($this->vars->e);
1446
1447        try {
1448            Kronolith::toTimeslice($event, $this->vars->t, $this->vars->c);
1449        } catch (Kronolith_Exception $e) {
1450            $GLOBALS['notification']->push(sprintf(_("Error saving timeslice: %s"), $e->getMessage()), 'horde.error');
1451            return false;
1452        }
1453        $GLOBALS['notification']->push(_("Successfully saved timeslice."), 'horde.success');
1454
1455        return true;
1456    }
1457
1458    /**
1459     * Check reply status of any resources and report back. Used as a check
1460     * before saving an event to give the user feedback.
1461     *
1462     * The following arguments are expected:
1463     *   - r:  A comma separated string of resource identifiers.
1464     *   - s:  The event start time to check.
1465     *   - e:  The event end time to check.
1466     *   - u:  The event uid, if not a new event.
1467     *   - c:  The event's calendar.
1468     */
1469    public function checkResources()
1470    {
1471        if (empty($GLOBALS['conf']['resource']['driver'])) {
1472            return array();
1473        }
1474
1475        if ($this->vars->i) {
1476            $event = $this->_getDriver($this->vars->c)->getEvent($this->vars->i);
1477        } else {
1478            $event = Kronolith::getDriver()->getEvent();
1479        }
1480        // Overrite start/end times since we may be checking before we edit
1481        // an existing event with new times.
1482        $event->start = new Horde_Date($this->vars->s);
1483        $event->end = new Horde_Date($this->vars->e);
1484        $event->start->setTimezone(date_default_timezone_get());
1485        $event->end->setTimezone(date_default_timezone_get());
1486        $results = array();
1487        foreach (explode(',', $this->vars->r) as $id) {
1488            $resource = Kronolith::getDriver('Resource')->getResource($id);
1489            $results[$id] = $resource->getResponse($event);
1490        }
1491
1492        return $results;
1493    }
1494
1495    /**
1496     * Returns the driver object for a calendar.
1497     *
1498     * @param string $cal  A calendar string in the format "type|name".
1499     *
1500     * @return Kronolith_Driver|boolean  A driver instance or false on failure.
1501     */
1502    protected function _getDriver($cal)
1503    {
1504        list($driver, $calendar) = explode('|', $cal);
1505        if ($driver == 'internal' &&
1506            !Kronolith::hasPermission($calendar, Horde_Perms::SHOW)) {
1507            $GLOBALS['notification']->push(_("Permission Denied"), 'horde.error');
1508            return false;
1509        }
1510        try {
1511            $kronolith_driver = Kronolith::getDriver($driver, $calendar);
1512        } catch (Exception $e) {
1513            $GLOBALS['notification']->push($e, 'horde.error');
1514            return false;
1515        }
1516        if ($driver == 'remote') {
1517            $kronolith_driver->setParam('timeout', 15);
1518        }
1519        return $kronolith_driver;
1520    }
1521
1522    /**
1523     * Saves an event and returns a signed result object including the saved
1524     * event.
1525     *
1526     * @param Kronolith_Event $event     An event object.
1527     * @param Kronolith_Event $original  If $event is an exception, this should
1528     *                                   be set to the original event.
1529     * @param object $attributes         The attributes sent by the client.
1530     *                                   Expected to contain cstart and cend.
1531     * @param boolean $saveOriginal      Commit any changes in $original to
1532     *                                   storage also.
1533     *
1534     * @return object  The result object.
1535     */
1536    protected function _saveEvent(Kronolith_Event $event,
1537                                  Kronolith_Event $original = null,
1538                                  $attributes = null,
1539                                  $saveOriginal = false)
1540    {
1541        if ($this->vars->targetcalendar) {
1542            $cal = $this->vars->targetcalendar;
1543        } elseif ($this->vars->cal) {
1544            $cal = $this->vars->cal;
1545        } else {
1546            $cal = $event->calendarType . '|' . $event->calendar;
1547        }
1548        $result = $this->_signedResponse($cal);
1549        $events = array();
1550        try {
1551            $event->save();
1552            if (!$this->vars->view_start || !$this->vars->view_end) {
1553              $result->events = array();
1554              return $result;
1555            }
1556            $end = new Horde_Date($this->vars->view_end);
1557            $end->hour = 23;
1558            $end->min = $end->sec = 59;
1559            Kronolith::addEvents(
1560                $events, $event,
1561                new Horde_Date($this->vars->view_start),
1562                $end, true, true);
1563            // If this is an exception, we re-add the original event also;
1564            // cstart and cend are the cacheStart and cacheEnd dates from the
1565            // client.
1566            if (!empty($original)) {
1567                Kronolith::addEvents(
1568                    $events, $original,
1569                    new Horde_Date($attributes->cstart),
1570                    new Horde_Date($attributes->cend),
1571                    true, true);
1572                if ($saveOriginal) {
1573                    $original->save();
1574                }
1575            }
1576
1577            // If this event recurs, we must add any bound exceptions to the
1578            // results
1579            if ($event->recurs()) {
1580                $bound = $event->boundExceptions(false);
1581                foreach ($bound as $day => &$exceptions) {
1582                    foreach ($exceptions as &$exception) {
1583                        $exception = $exception->toJson();
1584                    }
1585                }
1586                Kronolith::mergeEvents($events, $bound);
1587            }
1588            $result->events = count($events) ? $events : array();
1589        } catch (Exception $e) {
1590            $GLOBALS['notification']->push($e, 'horde.error');
1591        }
1592        return $result;
1593    }
1594
1595    /**
1596     * Creates a result object with the signature of the current request.
1597     *
1598     * @param string $calendar  A calendar id.
1599     *
1600     * @return object  The result object.
1601     */
1602    protected function _signedResponse($calendar)
1603    {
1604        $result = new stdClass;
1605        $result->cal = $calendar;
1606        $result->view = $this->vars->view;
1607        $result->sig = $this->vars->sig;
1608        return $result;
1609    }
1610
1611    /**
1612     * Add an exception to the original event.
1613     *
1614     * @param Kronolith_Event $event  The recurring event.
1615     * @param object $attributes      The attributes passed from the client.
1616     *                                Expected to contain either rstart or rday.
1617     *
1618     * @return Kronolith_Event  The event representing the exception, with
1619     *                          the start/end times set the same as the original
1620     *                          occurence.
1621     */
1622    protected function _addException(Kronolith_Event $event, $attributes)
1623    {
1624        if ($attributes->rstart) {
1625            $rstart = new Horde_Date($attributes->rstart);
1626            $rstart->setTimezone($event->start->timezone);
1627        } else {
1628            $rstart = new Horde_Date($attributes->rday);
1629            $rstart->setTimezone($event->start->timezone);
1630            $rstart->hour = $event->start->hour;
1631            $rstart->min = $event->start->min;
1632        }
1633        $event->recurrence->addException($rstart->year, $rstart->month, $rstart->mday);
1634        $event->save();
1635    }
1636
1637    /**
1638     * Creates a new event that represents an exception to a recurring event.
1639     *
1640     * @param Kronolith_Event $event  The original recurring event.
1641     * @param Kronolith_Event $copy   If present, contains a copy of $event, but
1642     *                                with changes from edited event form.
1643     * @param stdClass $attributes    The attributes passed from the client.
1644     *                                Expected to contain rstart and rend or
1645     *                                rday that represents the original
1646     *                                starting/ending date of the instance.
1647     *
1648     * @return Kronolith_Event  The event representing the exception
1649     */
1650    protected function _copyEvent(Kronolith_Event $event, Kronolith_Event $copy = null, $attributes = null)
1651    {
1652        if (empty($copy)) {
1653            $copy = clone($event);
1654        }
1655
1656        if ($attributes->rstart) {
1657            $rstart = new Horde_Date($attributes->rstart);
1658            $rstart->setTimezone($event->start->timezone);
1659            $rend = new Horde_Date($attributes->rend);
1660            $rend->setTimezone($event->end->timezone);
1661        } else {
1662            $rstart = new Horde_Date($attributes->rday);
1663            $rstart->setTimezone($event->start->timezone);
1664            $rstart->hour = $event->start->hour;
1665            $rstart->min = $event->start->min;
1666            $rend = $rstart->add($event->getDuration);
1667            $rend->setTimezone($event->end->timezone);
1668            $rend->hour = $event->end->hour;
1669            $rend->min = $event->end->min;
1670        }
1671        $uid = $event->uid;
1672        $otime = $event->start->strftime('%T');
1673
1674        // Create new event for the exception
1675        $nevent = $event->getDriver()->getEvent();
1676        $nevent->baseid = $uid;
1677        $nevent->exceptionoriginaldate = new Horde_Date($rstart->strftime('%Y-%m-%d') . 'T' . $otime);
1678        $nevent->exceptionoriginaldate->setTimezone($event->start->timezone);
1679        $nevent->creator = $event->creator;
1680        $nevent->title = $copy->title;
1681        $nevent->description = $copy->description;
1682        $nevent->location = $copy->location;
1683        $nevent->private = $copy->private;
1684        $nevent->url = $copy->url;
1685        $nevent->status = $copy->status;
1686        $nevent->attendees = $copy->attendees;
1687        $nevent->setResources($copy->getResources());
1688        $nevent->start = $rstart;
1689        $nevent->end = $rend;
1690        $nevent->initialized = true;
1691
1692        return $nevent;
1693    }
1694
1695}
1696