Overview

Packages

  • Kronolith
  • None

Classes

  • Kronolith
  • Kronolith_Ajax_Application
  • Kronolith_Ajax_Imple_ContactAutoCompleter
  • Kronolith_Ajax_Imple_Embed
  • Kronolith_Ajax_Imple_TagActions
  • Kronolith_Ajax_Imple_TagAutoCompleter
  • Kronolith_Api
  • Kronolith_Calendar
  • Kronolith_Calendar_External
  • Kronolith_Calendar_External_Tasks
  • Kronolith_Calendar_Holiday
  • Kronolith_Calendar_Internal
  • Kronolith_Calendar_Remote
  • Kronolith_Calendar_Resource
  • Kronolith_Calendars_Base
  • Kronolith_Calendars_Default
  • Kronolith_Calendars_Kolab
  • Kronolith_Day
  • Kronolith_Driver
  • Kronolith_Driver_Holidays
  • Kronolith_Driver_Horde
  • Kronolith_Driver_Ical
  • Kronolith_Driver_Kolab
  • Kronolith_Driver_Mock
  • Kronolith_Driver_Resource
  • Kronolith_Driver_Sql
  • Kronolith_Event
  • Kronolith_Event_Holidays
  • Kronolith_Event_Horde
  • Kronolith_Event_Ical
  • Kronolith_Event_Kolab
  • Kronolith_Event_Resource
  • Kronolith_Event_Sql
  • Kronolith_Exception
  • Kronolith_Factory_Calendars
  • Kronolith_Factory_Geo
  • Kronolith_Form_CreateCalendar
  • Kronolith_Form_CreateResource
  • Kronolith_Form_CreateResourceGroup
  • Kronolith_Form_DeleteCalendar
  • Kronolith_Form_DeleteResource
  • Kronolith_Form_DeleteResourceGroup
  • Kronolith_Form_EditCalendar
  • Kronolith_Form_EditRemoteCalendar
  • Kronolith_Form_EditResource
  • Kronolith_Form_EditResourceGroup
  • Kronolith_Form_SubscribeRemoteCalendar
  • Kronolith_Form_UnsubscribeRemoteCalendar
  • Kronolith_FreeBusy
  • Kronolith_FreeBusy_View
  • Kronolith_FreeBusy_View_Day
  • Kronolith_FreeBusy_View_Month
  • Kronolith_FreeBusy_View_Week
  • Kronolith_FreeBusy_View_Workweek
  • Kronolith_Geo_Base
  • Kronolith_Geo_Mysql
  • Kronolith_Geo_Sql
  • Kronolith_LoginTasks_SystemTask_Upgrade
  • Kronolith_LoginTasks_Task_PurgeEvents
  • Kronolith_Notification_Listener_AjaxStatus
  • Kronolith_Resource
  • Kronolith_Resource_Base
  • Kronolith_Resource_Group
  • Kronolith_Resource_Single
  • Kronolith_Storage
  • Kronolith_Storage_Kolab
  • Kronolith_Storage_Sql
  • Kronolith_Tagger
  • Kronolith_Test
  • Kronolith_View_Day
  • Kronolith_View_DeleteEvent
  • Kronolith_View_EditEvent
  • Kronolith_View_Event
  • Kronolith_View_ExportEvent
  • Kronolith_View_Month
  • Kronolith_View_Week
  • Kronolith_View_WorkWeek
  • Kronolith_View_Year
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * Kronolith_Event defines a generic API for events.
   4:  *
   5:  * Copyright 1999-2012 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:  */
  14: abstract 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 status of this event.
  77:      *
  78:      * @var integer
  79:      */
  80:     public $status = Kronolith::STATUS_CONFIRMED;
  81: 
  82:     /**
  83:      * URL to an icon of this event.
  84:      *
  85:      * @var string
  86:      */
  87:     public $icon = '';
  88: 
  89:     /**
  90:      * The description for this event.
  91:      *
  92:      * @var string
  93:      */
  94:     public $description = '';
  95: 
  96:     /**
  97:      * URL of this event.
  98:      *
  99:      * @var string
 100:      */
 101:     public $url = '';
 102: 
 103:     /**
 104:      * Whether the event is private.
 105:      *
 106:      * @var boolean
 107:      */
 108:     public $private = false;
 109: 
 110:     /**
 111:      * This tag's events.
 112:      *
 113:      * @var array|string
 114:      */
 115:     protected $_tags = null;
 116: 
 117:     /**
 118:      * Geolocation
 119:      *
 120:      * @var array
 121:      */
 122:     protected $_geoLocation;
 123: 
 124:     /**
 125:      * Whether this is the event on the first day of a multi-day event.
 126:      *
 127:      * @var boolen
 128:      */
 129:     public $first = true;
 130: 
 131:     /**
 132:      * Whether this is the event on the last day of a multi-day event.
 133:      *
 134:      * @var boolen
 135:      */
 136:     public $last = true;
 137: 
 138:     /**
 139:      * All the attendees of this event.
 140:      *
 141:      * This is an associative array where the keys are the email addresses
 142:      * of the attendees, and the values are also associative arrays with
 143:      * keys 'attendance' and 'response' pointing to the attendees' attendance
 144:      * and response values, respectively.
 145:      *
 146:      * @var array
 147:      */
 148:     public $attendees = array();
 149: 
 150:     /**
 151:      * All resources of this event.
 152:      *
 153:      * This is an associative array where keys are resource uids values are
 154:      * associative arrays with keys attendance and response.
 155:      *
 156:      * @var unknown_type
 157:      */
 158:     protected $_resources = array();
 159: 
 160:     /**
 161:      * The start time of the event.
 162:      *
 163:      * @var Horde_Date
 164:      */
 165:     public $start;
 166: 
 167:     /**
 168:      * The end time of the event.
 169:      *
 170:      * @var Horde_Date
 171:      */
 172:     public $end;
 173: 
 174:     /**
 175:      * The duration of this event in minutes
 176:      *
 177:      * @var integer
 178:      */
 179:     public $durMin = 0;
 180: 
 181:     /**
 182:      * Whether this is an all-day event.
 183:      *
 184:      * @var boolean
 185:      */
 186:     public $allday = false;
 187: 
 188:     /**
 189:      * Number of minutes before the event starts to trigger an alarm.
 190:      *
 191:      * @var integer
 192:      */
 193:     public $alarm = 0;
 194: 
 195:     /**
 196:      * Snooze minutes for this event's alarm.
 197:      *
 198:      * @see Horde_Alarm::snooze()
 199:      *
 200:      * @var integer
 201:      */
 202:     protected $_snooze;
 203: 
 204:     /**
 205:      * The particular alarm methods overridden for this event.
 206:      *
 207:      * @var array
 208:      */
 209:     public $methods;
 210: 
 211:     /**
 212:      * The identifier of the calender this event exists on.
 213:      *
 214:      * @var string
 215:      */
 216:     public $calendar;
 217: 
 218:     /**
 219:      * The type of the calender this event exists on.
 220:      *
 221:      * @var string
 222:      */
 223:     public $calendarType;
 224: 
 225:     /**
 226:      * The HTML background color to be used for this event.
 227:      *
 228:      * @var string
 229:      */
 230:     protected $_backgroundColor = '#dddddd';
 231: 
 232:     /**
 233:      * The HTML foreground color to be used for this event.
 234:      *
 235:      * @var string
 236:      */
 237:     protected $_foregroundColor = '#000000';
 238: 
 239:     /**
 240:      * The VarRenderer class to use for printing select elements.
 241:      *
 242:      * @var Horde_Core_Ui_VarRenderer
 243:      */
 244:     private $_varRenderer;
 245: 
 246:     /**
 247:      * The Horde_Date_Recurrence class for this event.
 248:      *
 249:      * @var Horde_Date_Recurrence
 250:      */
 251:     public $recurrence;
 252: 
 253:     /**
 254:      * Used in view renderers.
 255:      *
 256:      * @var integer
 257:      */
 258:     protected $_overlap;
 259: 
 260:     /**
 261:      * Used in view renderers.
 262:      *
 263:      * @var integer
 264:      */
 265:     protected $_indent;
 266: 
 267:     /**
 268:      * Used in view renderers.
 269:      *
 270:      * @var integer
 271:      */
 272:     protected $_span;
 273: 
 274:     /**
 275:      * Used in view renderers.
 276:      *
 277:      * @var integer
 278:      */
 279:     protected $_rowspan;
 280: 
 281:     /**
 282:      * The baseid. For events that represent exceptions this is the UID of the
 283:      * original, recurring event.
 284:      *
 285:      * @var string
 286:      */
 287:     public $baseid;
 288: 
 289:     /**
 290:      * For exceptions, the date of the original recurring event that this is an
 291:      * exception for.
 292:      *
 293:      * @var Horde_Date
 294:      */
 295:     public $exceptionoriginaldate;
 296: 
 297:     /**
 298:      * The cached event duration, split up in time units.
 299:      *
 300:      * @see getDuration()
 301:      * @var stdClass
 302:      */
 303:     protected $_duration;
 304: 
 305:     /**
 306:      * Constructor.
 307:      *
 308:      * @param Kronolith_Driver $driver  The backend driver that this event is
 309:      *                                  stored in.
 310:      * @param mixed $eventObject        Backend specific event object
 311:      *                                  that this will represent.
 312:      */
 313:     public function __construct($driver, $eventObject = null)
 314:     {
 315:         $this->calendar = $driver->calendar;
 316:         list($this->_backgroundColor, $this->_foregroundColor) = $driver->colors();
 317: 
 318:         if (!is_null($eventObject)) {
 319:             $this->fromDriver($eventObject);
 320:         }
 321:     }
 322: 
 323:     /**
 324:      * Setter.
 325:      *
 326:      * Sets the 'id' and 'creator' properties.
 327:      *
 328:      * @param string $name  Property name.
 329:      * @param mixed $value  Property value.
 330:      */
 331:     public function __set($name, $value)
 332:     {
 333:         switch ($name) {
 334:         case 'id':
 335:             if (substr($value, 0, 10) == 'kronolith:') {
 336:                 $value = substr($value, 10);
 337:             }
 338:             // Fall through.
 339:         case 'creator':
 340:         case 'overlap':
 341:         case 'indent':
 342:         case 'span':
 343:         case 'rowspan':
 344:         case 'geoLocation':
 345:         case 'tags':
 346:             $this->{'_' . $name} = $value;
 347:             return;
 348:         }
 349:         $trace = debug_backtrace();
 350:         trigger_error('Undefined property via __set(): ' . $name
 351:                       . ' in ' . $trace[0]['file']
 352:                       . ' on line ' . $trace[0]['line'],
 353:                       E_USER_NOTICE);
 354:     }
 355: 
 356:     /**
 357:      * Getter.
 358:      *
 359:      * Returns the 'id' and 'creator' properties.
 360:      *
 361:      * @param string $name  Property name.
 362:      *
 363:      * @return mixed  Property value.
 364:      */
 365:     public function __get($name)
 366:     {
 367:         switch ($name) {
 368:         case 'creator':
 369:             if (empty($this->_creator)) {
 370:                 $this->_creator = $GLOBALS['registry']->getAuth();
 371:             }
 372:             // Fall through.
 373:         case 'id':
 374:         case 'overlap':
 375:         case 'indent':
 376:         case 'span':
 377:         case 'rowspan':
 378:             return $this->{'_' . $name};
 379:         case 'tags':
 380:             if (!isset($this->_tags)) {
 381:                 $this->_tags = Kronolith::getTagger()->getTags($this->uid, 'event');
 382:             }
 383:             return $this->_tags;
 384:         case 'geoLocation':
 385:             if (!isset($this->_geoLocation)) {
 386:                 try {
 387:                     $this->_geoLocation = $GLOBALS['injector']->getInstance('Kronolith_Geo')->getLocation($this->id);
 388:                 } catch (Kronolith_Exception $e) {}
 389:             }
 390:             return $this->_geoLocation;
 391:         }
 392: 
 393:         $trace = debug_backtrace();
 394:         trigger_error('Undefined property via __set(): ' . $name
 395:                       . ' in ' . $trace[0]['file']
 396:                       . ' on line ' . $trace[0]['line'],
 397:                       E_USER_NOTICE);
 398:         return null;
 399:     }
 400: 
 401:     /**
 402:      * Returns a reference to a driver that's valid for this event.
 403:      *
 404:      * @return Kronolith_Driver  A driver that this event can use to save
 405:      *                           itself, etc.
 406:      */
 407:     public function getDriver()
 408:     {
 409:         return Kronolith::getDriver(str_replace('Kronolith_Event_', '', get_class($this)), $this->calendar);
 410:     }
 411: 
 412:     /**
 413:      * Returns the share this event belongs to.
 414:      *
 415:      * @return Horde_Share  This event's share.
 416:      * @throws Kronolith_Exception
 417:      */
 418:     public function getShare()
 419:     {
 420:         if (isset($GLOBALS['all_calendars'][$this->calendar])) {
 421:             return $GLOBALS['all_calendars'][$this->calendar]->share();
 422:         }
 423:         throw new Kronolith_Exception('Share not found');
 424:     }
 425: 
 426:     /**
 427:      * Encapsulates permissions checking.
 428:      *
 429:      * @param integer $permission  The permission to check for.
 430:      * @param string $user         The user to check permissions for.
 431:      *
 432:      * @return boolean
 433:      */
 434:     public function hasPermission($permission, $user = null)
 435:     {
 436:         if ($user === null) {
 437:             $user = $GLOBALS['registry']->getAuth();
 438:         }
 439:         try {
 440:             $share = $this->getShare();
 441:         } catch (Exception $e) {
 442:             return false;
 443:         }
 444:         return $share->hasPermission($user, $permission, $this->creator);
 445:     }
 446: 
 447:     /**
 448:      * Saves changes to this event.
 449:      *
 450:      * @return integer  The event id.
 451:      * @throws Kronolith_Exception
 452:      */
 453:     public function save()
 454:     {
 455:         if (!$this->initialized) {
 456:             throw new Kronolith_Exception('Event not yet initialized');
 457:         }
 458: 
 459:         /* Check for acceptance/denial of this event's resources. */
 460:         $add_events = array();
 461:         $locks = $GLOBALS['injector']->getInstance('Horde_Lock');
 462:         $lock = array();
 463:         $failed_resources = array();
 464:         foreach ($this->getResources() as $id => $resourceData) {
 465:             /* Get the resource and protect against infinite recursion in case
 466:              * someone is silly enough to add a resource to it's own event.*/
 467:             $resource = Kronolith::getDriver('Resource')->getResource($id);
 468:             $rcal = $resource->get('calendar');
 469:             if ($rcal == $this->calendar) {
 470:                 continue;
 471:             }
 472: 
 473:             /* Lock the resource and get the response */
 474:             if ($resource->get('response_type') == Kronolith_Resource::RESPONSETYPE_AUTO) {
 475:                 $principle = 'calendar/' . $rcal;
 476:                 $lock[$resource->getId()] = $locks->setLock($GLOBALS['registry']->getAuth(), 'kronolith', $principle, 5, Horde_Lock::TYPE_EXCLUSIVE);
 477:                 $haveLock = true;
 478:             } else {
 479:                 $haveLock = false;
 480:             }
 481:             if ($haveLock && !$lock[$resource->getId()]) {
 482:                 // Already locked
 483:                 // For now, just fail. Not sure how else to capture the locked
 484:                 // resources and notify the user.
 485:                 throw new Kronolith_Exception(sprintf(_("The resource \"%s\" was locked. Please try again."), $resource->get('name')));
 486:             } else {
 487:                 $response = $resource->getResponse($this);
 488:             }
 489: 
 490:             /* Remember accepted resources so we can add the event to their
 491:              * calendars. Otherwise, clear the lock. */
 492:             if ($response == Kronolith::RESPONSE_ACCEPTED) {
 493:                 $add_events[] = $resource;
 494:             } else {
 495:                 $locks->clearLock($lock[$resource->getId()]);
 496:             }
 497: 
 498:             /* Add the resource to the event */
 499:             $this->addResource($resource, $response);
 500:         }
 501: 
 502:         /* Save */
 503:         $result = $this->getDriver()->saveEvent($this);
 504: 
 505:         /* Now that the event is definitely commited to storage, we can add
 506:          * the event to each resource that has accepted. Not very efficient,
 507:          * but this also solves the problem of not having a GUID for the event
 508:          * until after it's saved. If we add the event to the resources
 509:          * calendar before it is saved, they will have different GUIDs, and
 510:          * hence no longer refer to the same event. */
 511:         foreach ($add_events as $resource) {
 512:             $resource->addEvent($this);
 513:             if ($resource->get('response_type') == Kronolith_Resource::RESPONSETYPE_AUTO) {
 514:                 $locks->clearLock($lock[$resource->getId()]);
 515:             }
 516:         }
 517: 
 518:         $hordeAlarm = $GLOBALS['injector']->getInstance('Horde_Alarm');
 519:         if ($alarm = $this->toAlarm(new Horde_Date($_SERVER['REQUEST_TIME']))) {
 520:             $alarm['start'] = new Horde_Date($alarm['start']);
 521:             $alarm['end'] = new Horde_Date($alarm['end']);
 522:             $hordeAlarm->set($alarm);
 523:             if ($this->_snooze) {
 524:                 $hordeAlarm->snooze($this->uid, $GLOBALS['registry']->getAuth(), $this->_snooze);
 525:             }
 526:         } else {
 527:             $hordeAlarm->delete($this->uid);
 528:         }
 529: 
 530:         return $result;
 531:     }
 532: 
 533:     /**
 534:      * Imports a backend specific event object.
 535:      *
 536:      * @param mixed $eventObject  Backend specific event object that this
 537:      *                            object will represent.
 538:      */
 539:     public function fromDriver($event)
 540:     {
 541:     }
 542: 
 543:     /**
 544:      * Exports this event in iCalendar format.
 545:      *
 546:      * @param Horde_Icalendar $calendar  A Horde_Icalendar object that acts as
 547:      *                                   a container.
 548:      *
 549:      * @return array  An array of Horde_Icalendar_Vevent objects for this event.
 550:      */
 551:     public function toiCalendar($calendar)
 552:     {
 553:         $vEvent = Horde_Icalendar::newComponent('vevent', $calendar);
 554:         $v1 = $calendar->getAttribute('VERSION') == '1.0';
 555:         $vEvents = array();
 556: 
 557:         /* DTEND is non-inclusive, but $this->end is inclusive. */
 558:         $end = clone $this->end;
 559:         $end->sec++;
 560:         if ($this->isAllDay()) {
 561:             $vEvent->setAttribute('DTSTART', $this->start, array('VALUE' => 'DATE'));
 562:             $vEvent->setAttribute('DTEND', $end, array('VALUE' => 'DATE'));
 563:             $vEvent->setAttribute('X-FUNAMBOL-ALLDAY', 1);
 564:         } else {
 565:             $vEvent->setAttribute('DTSTART', $this->start);
 566:             $vEvent->setAttribute('DTEND', $end);
 567:         }
 568: 
 569:         $vEvent->setAttribute('DTSTAMP', $_SERVER['REQUEST_TIME']);
 570:         $vEvent->setAttribute('UID', $this->uid);
 571: 
 572:         /* Get the event's history. */
 573:         $created = $modified = null;
 574:         try {
 575:             $log = $GLOBALS['injector']->getInstance('Horde_History')->getHistory('kronolith:' . $this->calendar . ':' . $this->uid);
 576:             foreach ($log as $entry) {
 577:                 switch ($entry['action']) {
 578:                 case 'add':
 579:                     $created = $entry['ts'];
 580:                     break;
 581: 
 582:                 case 'modify':
 583:                     $modified = $entry['ts'];
 584:                     break;
 585:                 }
 586:             }
 587:         } catch (Exception $e) {}
 588:         if (!empty($created)) {
 589:             $vEvent->setAttribute($v1 ? 'DCREATED' : 'CREATED', $created);
 590:             if (empty($modified)) {
 591:                 $modified = $created;
 592:             }
 593:         }
 594:         if (!empty($modified)) {
 595:             $vEvent->setAttribute('LAST-MODIFIED', $modified);
 596:         }
 597: 
 598:         $vEvent->setAttribute('SUMMARY', $this->getTitle());
 599: 
 600:         // Organizer
 601:         if (count($this->attendees)) {
 602:             $name = Kronolith::getUserName($this->creator);
 603:             $email = Kronolith::getUserEmail($this->creator);
 604:             $params = array();
 605:             if ($v1) {
 606:                 if (!empty($name)) {
 607:                     if (!empty($email)) {
 608:                         $email = ' <' . $email . '>';
 609:                     }
 610:                     $email = $name . $email;
 611:                     $email = Horde_Mime_Address::trimAddress($email);
 612:                 }
 613:             } else {
 614:                 if (!empty($name)) {
 615:                     $params['CN'] = $name;
 616:                 }
 617:                 if (!empty($email)) {
 618:                     $email = 'mailto:' . $email;
 619:                 }
 620:             }
 621:             $vEvent->setAttribute('ORGANIZER', $email, $params);
 622:         }
 623:         if (!$this->isPrivate()) {
 624:             if (!empty($this->description)) {
 625:                 $vEvent->setAttribute('DESCRIPTION', $this->description);
 626:             }
 627: 
 628:             // Tags
 629:             if ($this->tags) {
 630:                 $tags = implode(', ', $this->tags);
 631:                 $vEvent->setAttribute('CATEGORIES', $tags);
 632:             }
 633: 
 634:             // Location
 635:             if (!empty($this->location)) {
 636:                 $vEvent->setAttribute('LOCATION', $this->location);
 637:             }
 638:             if ($this->geoLocation) {
 639:                 $vEvent->setAttribute('GEO', array('latitude' => $this->geoLocation['lat'], 'longitude' => $this->geoLocation['lon']));
 640:             }
 641: 
 642:             // URL
 643:             if (!empty($this->url)) {
 644:                 $vEvent->setAttribute('URL', $this->url);
 645:             }
 646:         }
 647:         $vEvent->setAttribute('CLASS', $this->private ? 'PRIVATE' : 'PUBLIC');
 648: 
 649:         // Status.
 650:         switch ($this->status) {
 651:         case Kronolith::STATUS_FREE:
 652:             // This is not an official iCalendar value, but we need it for
 653:             // synchronization.
 654:             $vEvent->setAttribute('STATUS', 'FREE');
 655:             $vEvent->setAttribute('TRANSP', $v1 ? 1 : 'TRANSPARENT');
 656:             break;
 657:         case Kronolith::STATUS_TENTATIVE:
 658:             $vEvent->setAttribute('STATUS', 'TENTATIVE');
 659:             $vEvent->setAttribute('TRANSP', $v1 ? 0 : 'OPAQUE');
 660:             break;
 661:         case Kronolith::STATUS_CONFIRMED:
 662:             $vEvent->setAttribute('STATUS', 'CONFIRMED');
 663:             $vEvent->setAttribute('TRANSP', $v1 ? 0 : 'OPAQUE');
 664:             break;
 665:         case Kronolith::STATUS_CANCELLED:
 666:             if ($v1) {
 667:                 $vEvent->setAttribute('STATUS', 'DECLINED');
 668:                 $vEvent->setAttribute('TRANSP', 1);
 669:             } else {
 670:                 $vEvent->setAttribute('STATUS', 'CANCELLED');
 671:                 $vEvent->setAttribute('TRANSP', 'TRANSPARENT');
 672:             }
 673:             break;
 674:         }
 675: 
 676:         // Attendees.
 677:         foreach ($this->attendees as $email => $status) {
 678:             $params = array();
 679:             switch ($status['attendance']) {
 680:             case Kronolith::PART_REQUIRED:
 681:                 if ($v1) {
 682:                     $params['EXPECT'] = 'REQUIRE';
 683:                 } else {
 684:                     $params['ROLE'] = 'REQ-PARTICIPANT';
 685:                 }
 686:                 break;
 687: 
 688:             case Kronolith::PART_OPTIONAL:
 689:                 if ($v1) {
 690:                     $params['EXPECT'] = 'REQUEST';
 691:                 } else {
 692:                     $params['ROLE'] = 'OPT-PARTICIPANT';
 693:                 }
 694:                 break;
 695: 
 696:             case Kronolith::PART_NONE:
 697:                 if ($v1) {
 698:                     $params['EXPECT'] = 'FYI';
 699:                 } else {
 700:                     $params['ROLE'] = 'NON-PARTICIPANT';
 701:                 }
 702:                 break;
 703:             }
 704: 
 705:             switch ($status['response']) {
 706:             case Kronolith::RESPONSE_NONE:
 707:                 if ($v1) {
 708:                     $params['STATUS'] = 'NEEDS ACTION';
 709:                     $params['RSVP'] = 'YES';
 710:                 } else {
 711:                     $params['PARTSTAT'] = 'NEEDS-ACTION';
 712:                     $params['RSVP'] = 'TRUE';
 713:                 }
 714:                 break;
 715: 
 716:             case Kronolith::RESPONSE_ACCEPTED:
 717:                 if ($v1) {
 718:                     $params['STATUS'] = 'ACCEPTED';
 719:                 } else {
 720:                     $params['PARTSTAT'] = 'ACCEPTED';
 721:                 }
 722:                 break;
 723: 
 724:             case Kronolith::RESPONSE_DECLINED:
 725:                 if ($v1) {
 726:                     $params['STATUS'] = 'DECLINED';
 727:                 } else {
 728:                     $params['PARTSTAT'] = 'DECLINED';
 729:                 }
 730:                 break;
 731: 
 732:             case Kronolith::RESPONSE_TENTATIVE:
 733:                 if ($v1) {
 734:                     $params['STATUS'] = 'TENTATIVE';
 735:                 } else {
 736:                     $params['PARTSTAT'] = 'TENTATIVE';
 737:                 }
 738:                 break;
 739:             }
 740: 
 741:             if (strpos($email, '@') === false) {
 742:                 $email = '';
 743:             }
 744:             if ($v1) {
 745:                 if (!empty($status['name'])) {
 746:                     if (!empty($email)) {
 747:                         $email = ' <' . $email . '>';
 748:                     }
 749:                     $email = $status['name'] . $email;
 750:                     $email = Horde_Mime_Address::trimAddress($email);
 751:                 }
 752:             } else {
 753:                 if (!empty($status['name'])) {
 754:                     $params['CN'] = $status['name'];
 755:                 }
 756:                 if (!empty($email)) {
 757:                     $email = 'mailto:' . $email;
 758:                 }
 759:             }
 760: 
 761:             $vEvent->setAttribute('ATTENDEE', $email, $params);
 762:         }
 763: 
 764:         // Alarms.
 765:         if (!empty($this->alarm)) {
 766:             if ($v1) {
 767:                 $alarm = new Horde_Date($this->start);
 768:                 $alarm->min -= $this->alarm;
 769:                 $vEvent->setAttribute('AALARM', $alarm);
 770:             } else {
 771:                 $vAlarm = Horde_Icalendar::newComponent('valarm', $vEvent);
 772:                 $vAlarm->setAttribute('ACTION', 'DISPLAY');
 773:                 $vAlarm->setAttribute('DESCRIPTION', $this->getTitle());
 774:                 $vAlarm->setAttribute('TRIGGER;VALUE=DURATION', '-PT' . $this->alarm . 'M');
 775:                 $vEvent->addComponent($vAlarm);
 776:             }
 777:             $hordeAlarm = $GLOBALS['injector']->getInstance('Horde_Alarm');
 778:             if ($hordeAlarm->exists($this->uid, $GLOBALS['registry']->getAuth()) &&
 779:                 $hordeAlarm->isSnoozed($this->uid, $GLOBALS['registry']->getAuth())) {
 780:                 $vEvent->setAttribute('X-MOZ-LASTACK', new Horde_Date($_SERVER['REQUEST_TIME']));
 781:                 $alarm = $hordeAlarm->get($this->uid, $GLOBALS['registry']->getAuth());
 782:                 if (!empty($alarm['snooze'])) {
 783:                     $alarm['snooze']->setTimezone(date_default_timezone_get());
 784:                     $vEvent->setAttribute('X-MOZ-SNOOZE-TIME', $alarm['snooze']);
 785:                 }
 786:             }
 787:         }
 788: 
 789:         // Recurrence.
 790:         if ($this->recurs()) {
 791:             if ($v1) {
 792:                 $rrule = $this->recurrence->toRRule10($calendar);
 793:             } else {
 794:                 $rrule = $this->recurrence->toRRule20($calendar);
 795:             }
 796:             if (!empty($rrule)) {
 797:                 $vEvent->setAttribute('RRULE', $rrule);
 798:             }
 799: 
 800:             // Exceptions. An exception with no replacement event is represented
 801:             // by EXDATE, and those with replacement events are represented by
 802:             // a new vEvent element. We get all known replacement events first,
 803:             // then remove the exceptionoriginaldate from the list of the event
 804:             // exceptions. Any exceptions left should represent exceptions with
 805:             // no replacement.
 806:             $exceptions = $this->recurrence->getExceptions();
 807:             $kronolith_driver = Kronolith::getDriver(null, $this->calendar);
 808:             $search = new StdClass();
 809:             $search->baseid = $this->uid;
 810:             $results = $kronolith_driver->search($search);
 811:             $exdates = array();
 812:             foreach ($results as $days) {
 813:                 foreach ($days as $exceptionEvent) {
 814:                     // Need to change the UID so it links to the original
 815:                     // recurring event, but only if not using $v1. If using $v1,
 816:                     // we add the date to EXDATE and do NOT change the UID.
 817:                     if (!$v1) {
 818:                         $exceptionEvent->uid = $this->uid;
 819:                     }
 820:                     $vEventException = $exceptionEvent->toiCalendar($calendar);
 821: 
 822:                     // This should never happen, but protect against it anyway.
 823:                     if (count($vEventException) > 1) {
 824:                         throw new Kronolith_Exception(_("Unable to parse event."));
 825:                     }
 826:                     $vEventException = array_pop($vEventException);
 827:                     // If $v1, need to add to EXDATE and
 828:                     if (!$v1) {
 829:                         $vEventException->setAttribute('RECURRENCE-ID', $exceptionEvent->exceptionoriginaldate->timestamp());
 830:                     } else {
 831:                         $exdates[] = $exceptionEvent->exceptionoriginaldate;
 832:                     }
 833:                     $originaldate = $exceptionEvent->exceptionoriginaldate->format('Ymd');
 834:                     $key = array_search($originaldate, $exceptions);
 835:                     if ($key !== false) {
 836:                         unset($exceptions[$key]);
 837:                     }
 838:                     $vEvents[] = $vEventException;
 839:                 }
 840:             }
 841: 
 842:             /* The remaining exceptions represent deleted recurrences */
 843:             foreach ($exceptions as $exception) {
 844:                 if (!empty($exception)) {
 845:                     list($year, $month, $mday) = sscanf($exception, '%04d%02d%02d');
 846:                     $exdates[] = new Horde_Date($year, $month, $mday);
 847:                 }
 848:             }
 849:             if ($exdates) {
 850:                 $vEvent->setAttribute('EXDATE', $exdates);
 851:             }
 852:         }
 853:         array_unshift($vEvents, $vEvent);
 854: 
 855:         return $vEvents;
 856:     }
 857: 
 858:     /**
 859:      * Updates the properties of this event from a Horde_Icalendar_Vevent
 860:      * object.
 861:      *
 862:      * @param Horde_Icalendar_Vevent $vEvent  The iCalendar data to update
 863:      *                                        from.
 864:      */
 865:     public function fromiCalendar($vEvent)
 866:     {
 867:         // Unique ID.
 868:         try {
 869:             $uid = $vEvent->getAttribute('UID');
 870:             if (!empty($uid)) {
 871:                 $this->uid = $uid;
 872:             }
 873:         } catch (Horde_Icalendar_Exception $e) {}
 874: 
 875:         // Sequence.
 876:         try {
 877:             $seq = $vEvent->getAttribute('SEQUENCE');
 878:             if (is_int($seq)) {
 879:                 $this->sequence = $seq;
 880:             }
 881:         } catch (Horde_Icalendar_Exception $e) {}
 882: 
 883:         // Title, tags and description.
 884:         try {
 885:             $title = $vEvent->getAttribute('SUMMARY');
 886:             if (!is_array($title)) {
 887:                 $this->title = $title;
 888:             }
 889:         } catch (Horde_Icalendar_Exception $e) {}
 890: 
 891:         // Tags
 892:         try {
 893:             $this->_tags = $vEvent->getAttributeValues('CATEGORIES');
 894:         } catch (Horde_Icalendar_Exception $e) {}
 895: 
 896:         // Description
 897:         try {
 898:             $desc = $vEvent->getAttribute('DESCRIPTION');
 899:             if (!is_array($desc)) {
 900:                 $this->description = $desc;
 901:             }
 902:         } catch (Horde_Icalendar_Exception $e) {}
 903: 
 904:         // Remote Url
 905:         try {
 906:             $url = $vEvent->getAttribute('URL');
 907:             if (!is_array($url)) {
 908:                 $this->url = $url;
 909:             }
 910:         } catch (Horde_Icalendar_Exception $e) {}
 911: 
 912:         // Location
 913:         try {
 914:             $location = $vEvent->getAttribute('LOCATION');
 915:             if (!is_array($location)) {
 916:                 $this->location = $location;
 917:             }
 918:         } catch (Horde_Icalendar_Exception $e) {}
 919: 
 920:         try {
 921:             $geolocation = $vEvent->getAttribute('GEO');
 922:             $this->geoLocation = array(
 923:                 'lat' => $geolocation['latitude'],
 924:                 'lon' => $geolocation['longitude']
 925:             );
 926:         } catch (Horde_Icalendar_Exception $e) {}
 927: 
 928:         // Class
 929:         try {
 930:             $class = $vEvent->getAttribute('CLASS');
 931:             if (!is_array($class)) {
 932:                 $class = Horde_String::upper($class);
 933:                 $this->private = $class == 'PRIVATE' || $class == 'CONFIDENTIAL';
 934:             }
 935:         } catch (Horde_Icalendar_Exception $e) {}
 936: 
 937:         // Status.
 938:         try {
 939:             $status = $vEvent->getAttribute('STATUS');
 940:             if (!is_array($status)) {
 941:                 $status = Horde_String::upper($status);
 942:                 if ($status == 'DECLINED') {
 943:                     $status = 'CANCELLED';
 944:                 }
 945:                 if (defined('Kronolith::STATUS_' . $status)) {
 946:                     $this->status = constant('Kronolith::STATUS_' . $status);
 947:                 }
 948:             }
 949:         } catch (Horde_Icalendar_Exception $e) {}
 950: 
 951:         // Reset allday flag in case this has changed. Will be recalculated
 952:         // next time isAllDay() is called.
 953:         $this->allday = false;
 954: 
 955:         // Start and end date.
 956:         try {
 957:             $start = $vEvent->getAttribute('DTSTART');
 958:             if (!is_array($start)) {
 959:                 // Date-Time field
 960:                 $this->start = new Horde_Date($start);
 961:             } else {
 962:                 // Date field
 963:                 $this->start = new Horde_Date(
 964:                     array('year'  => (int)$start['year'],
 965:                           'month' => (int)$start['month'],
 966:                           'mday'  => (int)$start['mday']));
 967:             }
 968:         } catch (Horde_Icalendar_Exception $e) {}
 969: 
 970:         try {
 971:             $end = $vEvent->getAttribute('DTEND');
 972:             if (!is_array($end)) {
 973:                 // Date-Time field
 974:                 $this->end = new Horde_Date($end);
 975:                 // All day events are transferred by many device as
 976:                 // DSTART: YYYYMMDDT000000 DTEND: YYYYMMDDT2359(59|00)
 977:                 // Convert accordingly
 978:                 if (is_object($this->start) && $this->start->hour == 0 &&
 979:                     $this->start->min == 0 && $this->start->sec == 0 &&
 980:                     $this->end->hour == 23 && $this->end->min == 59) {
 981:                     $this->end = new Horde_Date(
 982:                         array('year'  => (int)$this->end->year,
 983:                               'month' => (int)$this->end->month,
 984:                               'mday'  => (int)$this->end->mday + 1));
 985:                 }
 986:             } else {
 987:                 // Date field
 988:                 $this->end = new Horde_Date(
 989:                     array('year'  => (int)$end['year'],
 990:                           'month' => (int)$end['month'],
 991:                           'mday'  => (int)$end['mday']));
 992:             }
 993:         } catch (Horde_Icalendar_Exception $e) {
 994:             $end = null;
 995:         }
 996: 
 997:         if (is_null($end)) {
 998:             try {
 999:                 $duration = $vEvent->getAttribute('DURATION');
1000:                 if (!is_array($duration)) {
1001:                     $this->end = new Horde_Date($this->start);
1002:                     $this->end->sec += $duration;
1003:                     $end = 1;
1004:                 }
1005:             } catch (Horde_Icalendar_Exception $e) {}
1006: 
1007:             if (is_null($end)) {
1008:                 // End date equal to start date as per RFC 2445.
1009:                 $this->end = new Horde_Date($this->start);
1010:                 if (is_array($start)) {
1011:                     // Date field
1012:                     $this->end->mday++;
1013:                 }
1014:             }
1015:         }
1016: 
1017:         // vCalendar 1.0 alarms
1018:         try {
1019:             $alarm = $vEvent->getAttribute('AALARM');
1020:             if (!is_array($alarm) && intval($alarm)) {
1021:                 $this->alarm = intval(($this->start->timestamp() - $alarm) / 60);
1022:             }
1023:         } catch (Horde_Icalendar_Exception $e) {}
1024: 
1025:         // vCalendar 2.0 alarms
1026:         foreach ($vEvent->getComponents() as $alarm) {
1027:             if (!($alarm instanceof Horde_Icalendar_Valarm)) {
1028:                 continue;
1029:             }
1030:             try {
1031:                 // @todo consider implementing different ACTION types.
1032:                 // $action = $alarm->getAttribute('ACTION');
1033:                 $trigger = $alarm->getAttribute('TRIGGER');
1034:                 $triggerParams = $alarm->getAttribute('TRIGGER', true);
1035:             } catch (Horde_Icalendar_Exception $e) {
1036:                 continue;
1037:             }
1038:             if (!is_array($triggerParams)) {
1039:                 $triggerParams = array($triggerParams);
1040:             }
1041:             foreach ($triggerParams as $tp) {
1042:                 if (isset($tp['VALUE']) &&
1043:                     $tp['VALUE'] == 'DATE-TIME') {
1044:                     if (isset($tp['RELATED']) &&
1045:                         $tp['RELATED'] == 'END') {
1046:                         $this->alarm = intval(($this->end->timestamp() - $trigger) / 60);
1047:                     } else {
1048:                         $this->alarm = intval(($this->start->timestamp() - $trigger) / 60);
1049:                     }
1050:                 } else {
1051:                     $this->alarm = -intval($trigger / 60);
1052:                     if (isset($tp['RELATED']) &&
1053:                         $tp['RELATED'] == 'END') {
1054:                         $this->alarm -= $this->durMin;
1055:                     }
1056:                 }
1057:             }
1058:         }
1059: 
1060:         // Alarm snoozing/dismissal
1061:         if ($this->alarm) {
1062:             try {
1063:                 // If X-MOZ-LASTACK is set, this event is either dismissed or
1064:                 // snoozed.
1065:                 $vEvent->getAttribute('X-MOZ-LASTACK');
1066:                 $hordeAlarm = $GLOBALS['injector']->getInstance('Horde_Alarm');
1067:                 try {
1068:                     // If X-MOZ-SNOOZE-TIME is set, this event is snoozed.
1069:                     $snooze = $vEvent->getAttribute('X-MOZ-SNOOZE-TIME');
1070:                     $this->_snooze = intval(($snooze - time()) / 60);
1071:                 } catch (Horde_Icalendar_Exception $e) {
1072:                     // If X-MOZ-SNOOZE-TIME is not set, this event is dismissed.
1073:                     $this->_snooze = -1;
1074:                 }
1075:             } catch (Horde_Icalendar_Exception $e) {
1076:             }
1077:         }
1078: 
1079:         // Attendance.
1080:         // Importing attendance may result in confusion: editing an imported
1081:         // copy of an event can cause invitation updates to be sent from
1082:         // people other than the original organizer. So we don't import by
1083:         // default. However to allow updates by SyncML replication, the custom
1084:         // X-ATTENDEE attribute is used which has the same syntax as
1085:         // ATTENDEE.
1086:         try {
1087:             $attendee = $vEvent->getAttribute('X-ATTENDEE');
1088:             if (!is_array($attendee)) {
1089:                 $attendee = array($attendee);
1090:             }
1091:             $params = $vEvent->getAttribute('X-ATTENDEE', true);
1092:             if (!is_array($params)) {
1093:                 $params = array($params);
1094:             }
1095:             for ($i = 0; $i < count($attendee); ++$i) {
1096:                 $attendee[$i] = str_replace(array('MAILTO:', 'mailto:'), '',
1097:                                             $attendee[$i]);
1098:                 $email = Horde_Mime_Address::bareAddress($attendee[$i]);
1099:                 // Default according to rfc2445:
1100:                 $attendance = Kronolith::PART_REQUIRED;
1101:                 // vCalendar 2.0 style:
1102:                 if (!empty($params[$i]['ROLE'])) {
1103:                     switch($params[$i]['ROLE']) {
1104:                     case 'OPT-PARTICIPANT':
1105:                         $attendance = Kronolith::PART_OPTIONAL;
1106:                         break;
1107: 
1108:                     case 'NON-PARTICIPANT':
1109:                         $attendance = Kronolith::PART_NONE;
1110:                         break;
1111:                     }
1112:                 }
1113:                 // vCalendar 1.0 style;
1114:                 if (!empty($params[$i]['EXPECT'])) {
1115:                     switch($params[$i]['EXPECT']) {
1116:                     case 'REQUEST':
1117:                         $attendance = Kronolith::PART_OPTIONAL;
1118:                         break;
1119: 
1120:                     case 'FYI':
1121:                         $attendance = Kronolith::PART_NONE;
1122:                         break;
1123:                     }
1124:                 }
1125:                 $response = Kronolith::RESPONSE_NONE;
1126:                 if (empty($params[$i]['PARTSTAT']) &&
1127:                     !empty($params[$i]['STATUS'])) {
1128:                     $params[$i]['PARTSTAT']  = $params[$i]['STATUS'];
1129:                 }
1130: 
1131:                 if (!empty($params[$i]['PARTSTAT'])) {
1132:                     switch($params[$i]['PARTSTAT']) {
1133:                     case 'ACCEPTED':
1134:                         $response = Kronolith::RESPONSE_ACCEPTED;
1135:                         break;
1136: 
1137:                     case 'DECLINED':
1138:                         $response = Kronolith::RESPONSE_DECLINED;
1139:                         break;
1140: 
1141:                     case 'TENTATIVE':
1142:                         $response = Kronolith::RESPONSE_TENTATIVE;
1143:                         break;
1144:                     }
1145:                 }
1146:                 $name = isset($params[$i]['CN']) ? $params[$i]['CN'] : null;
1147: 
1148:                 $this->addAttendee($email, $attendance, $response, $name);
1149:             }
1150:         } catch (Horde_Icalendar_Exception $e) {}
1151: 
1152:         $this->_handlevEventRecurrence($vEvent);
1153: 
1154:         $this->initialized = true;
1155:     }
1156: 
1157:     /**
1158:      * Handle parsing recurrence related fields.
1159:      *
1160:      * @param Horde_Icalendar $vEvent
1161:      */
1162:     protected function _handlevEventRecurrence($vEvent)
1163:     {
1164:         // Recurrence.
1165:         try {
1166:             $rrule = $vEvent->getAttribute('RRULE');
1167:             if (!is_array($rrule)) {
1168:                 $this->recurrence = new Horde_Date_Recurrence($this->start);
1169:                 if (strpos($rrule, '=') !== false) {
1170:                     $this->recurrence->fromRRule20($rrule);
1171:                 } else {
1172:                     $this->recurrence->fromRRule10($rrule);
1173:                 }
1174: 
1175:                 /* Delete all existing exceptions to this event if it already exists */
1176:                 if (!empty($this->uid)) {
1177:                     $kronolith_driver = Kronolith::getDriver(null, $this->calendar);
1178:                     $search = new StdClass();
1179:                     $search->start = $this->recurrence->getRecurStart();
1180:                     $search->end = $this->recurrence->getRecurEnd();
1181:                     $search->baseid = $this->uid;
1182:                     $results = $kronolith_driver->search($search);
1183:                     foreach ($results as $days) {
1184:                         foreach ($days as $exception) {
1185:                             $kronolith_driver->deleteEvent($exception->id);
1186:                         }
1187:                     }
1188:                 }
1189: 
1190:                 // Exceptions. EXDATE represents deleted events, just add the
1191:                 // exception, no new event is needed.
1192:                 $exdates = $vEvent->getAttributeValues('EXDATE');
1193:                 if (is_array($exdates)) {
1194:                     foreach ($exdates as $exdate) {
1195:                         if (is_array($exdate)) {
1196:                             $this->recurrence->addException(
1197:                                 (int)$exdate['year'],
1198:                                 (int)$exdate['month'],
1199:                                 (int)$exdate['mday']);
1200:                         }
1201:                     }
1202:                 }
1203:             }
1204:         } catch (Horde_Icalendar_Exception $e) {}
1205: 
1206:         // RECURRENCE-ID indicates that this event represents an exception
1207:         try {
1208:             $recurrenceid = $vEvent->getAttribute('RECURRENCE-ID');
1209:             $kronolith_driver = Kronolith::getDriver(null, $this->calendar);
1210:             $originaldt = new Horde_Date($recurrenceid);
1211:             $this->exceptionoriginaldate = $originaldt;
1212:             $this->baseid = $this->uid;
1213:             $this->uid = null;
1214:             $originalEvent = $kronolith_driver->getByUID($this->baseid);
1215:             $originalEvent->recurrence->addException($originaldt->format('Y'),
1216:                 $originaldt->format('m'),
1217:                 $originaldt->format('d'));
1218:             $originalEvent->save();
1219:         } catch (Horde_Icalendar_Exception $e) {}
1220:     }
1221: 
1222:     /**
1223:      * Imports the values for this event from a MS ActiveSync Message.
1224:      *
1225:      * @see Horde_ActiveSync_Message_Appointment
1226:      */
1227:     public function fromASAppointment(Horde_ActiveSync_Message_Appointment $message)
1228:     {
1229:         /* New event? */
1230:         if ($this->id === null) {
1231:             $this->creator = $GLOBALS['registry']->getAuth();
1232:         }
1233:         if (strlen($title = $message->getSubject())) {
1234:             $this->title = $title;
1235:         }
1236:         if (strlen($description = $message->getBody())) {
1237:             $this->description = $description;
1238:         }
1239:         if (strlen($location = $message->getLocation())) {
1240:             $this->location = $location;
1241:         }
1242: 
1243:         /* Date/times */
1244:         $tz = $message->getTimezone();
1245:         $dates = $message->getDatetime();
1246:         $this->start = clone($dates['start']);
1247:         $this->start->setTimezone($tz);
1248:         $this->end = clone($dates['end']);
1249:         $this->end->setTimezone($tz);
1250:         $this->allday = $dates['allday'];
1251: 
1252:         /* Sensitivity */
1253:         $this->private = ($message->getSensitivity() == Horde_ActiveSync_Message_Appointment::SENSITIVITY_PRIVATE || $message->getSensitivity() == Horde_ActiveSync_Message_Appointment::SENSITIVITY_CONFIDENTIAL) ? true :  false;
1254: 
1255:         /* Busy Status */
1256:         $status = $message->getBusyStatus();
1257:         switch ($status) {
1258:         case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_BUSY:
1259:             $status = Kronolith::STATUS_CONFIRMED;
1260:             break;
1261: 
1262:         case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_FREE:
1263:             $status = Kronolith::STATUS_FREE;
1264:             break;
1265: 
1266:         case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_TENTATIVE:
1267:             $status = Kronolith::STATUS_TENTATIVE;
1268:             break;
1269:         // @TODO: not sure how "Out" should show in kronolith...
1270:         case Horde_ActiveSync_Message_Appointment::BUSYSTATUS_OUT:
1271:             $status = Kronolith::STATUS_CONFIRMED;
1272:         default:
1273:             $status = Kronolith::STATUS_NONE;
1274:         }
1275:         $this->status = $status;
1276: 
1277:         /* Alarm */
1278:         if ($alarm = $message->getReminder()) {
1279:             $this->alarm = $alarm;
1280:         }
1281: 
1282:         /* Recurrence */
1283:         if ($rrule = $message->getRecurrence()) {
1284: 
1285:             /* Exceptions */
1286:             /* Since AS keeps exceptions as part of the original event, we need to
1287:              * delete all existing exceptions and re-create them. The only drawback
1288:              * to this is that the UIDs will change.
1289:              */
1290:             $this->recurrence = $rrule;
1291:             if (!empty($this->uid)) {
1292:                 $kronolith_driver = Kronolith::getDriver(null, $this->calendar);
1293:                 $search = new StdClass();
1294:                 $search->start = $rrule->getRecurStart();
1295:                 $search->end = $rrule->getRecurEnd();
1296:                 $search->baseid = $this->uid;
1297:                 $results = $kronolith_driver->search($search);
1298:                 foreach ($results as $days) {
1299:                     foreach ($days as $exception) {
1300:                         $kronolith_driver->deleteEvent($exception->id);
1301:                     }
1302:                 }
1303:             }
1304: 
1305:             $erules = $message->getExceptions();
1306:             foreach ($erules as $rule){
1307:                 /* Readd the exception event, but only if not deleted */
1308:                 if (!$rule->deleted) {
1309:                     $event = $kronolith_driver->getEvent();
1310:                     $times = $rule->getDatetime();
1311:                     $original = $rule->getExceptionStartTime();
1312:                     $this->recurrence->addException($original->format('Y'), $original->format('m'), $original->format('d'));
1313:                     $event->start = $times['start'];
1314:                     $event->end = $times['end'];
1315:                     $event->allday = $times['allday'];
1316:                     $event->title = $rule->getSubject();
1317:                     $event->description = $rule->getBody();
1318:                     $event->baseid = $this->uid;
1319:                     $event->exceptionoriginaldate = $original;
1320:                     $event->initialized = true;
1321:                     $event->save();
1322:                 } else {
1323:                     /* For exceptions that are deletions, just add the exception */
1324:                     $exceptiondt = $rule->getExceptionStartTime();
1325:                     $this->recurrence->addException($exceptiondt->format('Y'), $exceptiondt->format('m'), $exceptiondt->format('d'));
1326:                }
1327:             }
1328:         }
1329: 
1330:         /* Attendees */
1331:         $attendees = $message->getAttendees();
1332:         foreach ($attendees as $attendee) {
1333:             // TODO: participation and response are not supported in AS <= 2.5
1334:             $this->addAttendee($attendee->email,
1335:                                Kronolith::PART_NONE,
1336:                                Kronolith::RESPONSE_NONE,
1337:                                $attendee->name);
1338:         }
1339: 
1340:         /* Categories (Tags) */
1341:         $this->_tags = $message->getCategories();
1342: 
1343:         /* Flag that we are initialized */
1344:         $this->initialized = true;
1345:     }
1346: 
1347:     /**
1348:      * Export this event as a MS ActiveSync Message
1349:      *
1350:      * @return Horde_ActiveSync_Message_Appointment
1351:      */
1352:     public function toASAppointment()
1353:     {
1354:         $message = new Horde_ActiveSync_Message_Appointment(
1355:             array('logger' => $GLOBALS['injector']->getInstance('Horde_Log_Logger')));
1356:         $message->setSubject($this->getTitle());
1357:         if (!$this->isPrivate()) {
1358:             $message->setBody($this->description);
1359:             $message->setLocation($this->location);
1360:         }
1361: 
1362:         /* Start and End */
1363:         $message->setDatetime(array('start' => $this->start,
1364:                                     'end' => $this->end,
1365:                                     'allday' => $this->isAllDay()));
1366: 
1367:         /* Timezone */
1368:         $message->setTimezone($this->start);
1369: 
1370:         /* Organizer */
1371:         if (count($this->attendees)) {
1372:             $name = Kronolith::getUserName($this->creator);
1373:             $message->setOrganizer(
1374:                     array('name' => $name,
1375:                           'email' => Kronolith::getUserEmail($this->creator))
1376:             );
1377:         }
1378: 
1379:         /* Privacy */
1380:         $message->setSensitivity($this->private ?
1381:             Horde_ActiveSync_Message_Appointment::SENSITIVITY_PRIVATE :
1382:             Horde_ActiveSync_Message_Appointment::SENSITIVITY_NORMAL);
1383: 
1384:         /* Busy Status */
1385:         switch ($this->status) {
1386:         case Kronolith::STATUS_CANCELLED:
1387:             $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_FREE;
1388:             break;
1389:         case Kronolith::STATUS_CONFIRMED:
1390:             $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_BUSY;
1391:             break;
1392:         case Kronolith::STATUS_TENTATIVE:
1393:             $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_TENTATIVE;
1394:         case Kronolith::STATUS_FREE:
1395:         case Kronolith::STATUS_NONE:
1396:             $status = Horde_ActiveSync_Message_Appointment::BUSYSTATUS_FREE;
1397:         }
1398:         $message->setBusyStatus($status);
1399: 
1400:         /* DTStamp */
1401:         $message->setDTStamp($_SERVER['REQUEST_TIME']);
1402: 
1403:         /* Recurrence */
1404:         if ($this->recurs()) {
1405:             $message->setRecurrence($this->recurrence);
1406: 
1407:             /* Exceptions are tricky. Exceptions, even those that represent
1408:              * deleted instances of a recurring event, must be added. To do this
1409:              * we query the storage for all the events that represent exceptions
1410:              * (those with the baseid == $this->uid) and then remove the
1411:              * exceptionoriginaldate from the list of exceptions we know about.
1412:              * Any dates left in this list when we are done, must represent
1413:              * deleted instances of this recurring event.*/
1414:             if (!empty($this->recurrence) && $exceptions = $this->recurrence->getExceptions()) {
1415:                 $kronolith_driver = Kronolith::getDriver(null, $this->calendar);
1416:                 $search = new StdClass();
1417:                 $search->start = $this->recurrence->getRecurStart();
1418:                 $search->end = $this->recurrence->getRecurEnd();
1419:                 $search->baseid = $this->uid;
1420:                 $results = $kronolith_driver->search($search);
1421:                 foreach ($results as $days) {
1422:                     foreach ($days as $exception) {
1423:                         $e = new Horde_ActiveSync_Message_Exception();
1424:                         /* Times */
1425:                         $e->setDateTime(
1426:                             array('start' => $exception->start,
1427:                                   'end' => $exception->end,
1428:                                   'allday' => $exception->isAllDay()));
1429:                         /* The start time of the *original* recurring event */
1430:                         $e->setExceptionStartTime($exception->exceptionoriginaldate);
1431:                         $originaldate = $exception->exceptionoriginaldate->format('Ymd');
1432:                         $key = array_search($originaldate, $exceptions);
1433:                         if ($key !== false) {
1434:                             unset($exceptions[$key]);
1435:                         }
1436: 
1437:                         /* Remaining properties that could be different */
1438:                         $e->setSubject($exception->getTitle());
1439:                         if (!$exception->isPrivate()) {
1440:                             $e->setLocation($exception->location);
1441:                             $e->setBody($exception->description);
1442:                         }
1443: 
1444:                         $e->setSensitivity($exception->private ?
1445:                             Horde_ActiveSync_Message_Appointment::SENSITIVITY_PRIVATE :
1446:                             Horde_ActiveSync_Message_Appointment::SENSITIVITY_NORMAL);
1447: 
1448:                         $e->setReminder($exception->alarm);
1449:                         $e->setDTStamp($_SERVER['REQUEST_TIME']);
1450:                         /* Response Status */
1451:                         switch ($exception->status) {
1452:                         case Kronolith::STATUS_CANCELLED:
1453:                             $status = 'declined';
1454:                             break;
1455:                         case Kronolith::STATUS_CONFIRMED:
1456:                             $status = 'accepted';
1457:                             break;
1458:                         case Kronolith::STATUS_TENTATIVE:
1459:                             $status = 'tentative';
1460:                         case Kronolith::STATUS_FREE:
1461:                         case Kronolith::STATUS_NONE:
1462:                             $status = 'none';
1463:                         }
1464:                         $e->setResponseType($status);
1465: 
1466:                         /* Tags/Categories */
1467:                         if (!$exception->isPrivate()) {
1468:                             foreach ($exception->tags as $tag) {
1469:                                 $e->addCategory($tag);
1470:                             }
1471:                         }
1472:                         $message->addexception($e);
1473: 
1474:                     }
1475:                 }
1476: 
1477:                 /* Any dates left in $exceptions must be deleted exceptions */
1478:                 foreach ($exceptions as $deleted) {
1479:                     $e = new Horde_ActiveSync_Message_Exception();
1480:                     // Kronolith stores the date only, but some AS clients need
1481:                     // the datetime.
1482:                     $st = new Horde_Date($deleted);
1483:                     $st->hour = $this->start->hour;
1484:                     $st->min = $this->start->min;
1485:                     $e->setExceptionStartTime($st);
1486:                     $e->deleted = true;
1487:                     $message->addException($e);
1488:                 }
1489:             }
1490:         }
1491: 
1492:         /* Attendees */
1493:         if (!$this->isPrivate() && count($this->attendees)) {
1494:             $message->setMeetingStatus(Horde_ActiveSync_Message_Appointment::MEETING_IS_MEETING);
1495:             foreach ($this->attendees as $email => $properties) {
1496:                 $attendee = new Horde_ActiveSync_Message_Attendee();
1497:                 $attendee->email = $email;
1498:                 // AS only as required or optional
1499:                 //$attendee->type = ($properties['attendance'] !== Kronolith::PART_REQUIRED ? Kronolith::PART_OPTIONAL : Kronolith::PART_REQUIRED);
1500:                 //$attendee->status = $properties['response'];
1501:                 $message->addAttendee($attendee);
1502:             }
1503:         }
1504: 
1505: //        /* Resources */
1506: //        $r = $this->getResources();
1507: //        foreach ($r as $id => $data) {
1508: //            $resource = Kronolith::getDriver('Resource')->getResource($id);
1509: //            $attendee = new Horde_ActiveSync_Message_Attendee();
1510: //            $attendee->email = $resource->get('email');
1511: //            $attendee->type = Horde_ActiveSync_Message_Attendee::TYPE_RESOURCE;
1512: //            $attendee->name = $data['name'];
1513: //            $attendee->status = $data['response'];
1514: //            $message->addAttendee($attendee);
1515: //        }
1516: 
1517:         /* Reminder */
1518:         if ($this->alarm) {
1519:             $message->setReminder($this->alarm);
1520:         }
1521: 
1522:         /* Categories (tags) */
1523:         if (!$this->isPrivate()) {
1524:             foreach ($this->tags as $tag) {
1525:                 $message->addCategory($tag);
1526:             }
1527:         }
1528: 
1529:         return $message;
1530:     }
1531: 
1532:     /**
1533:      * Imports the values for this event from an array of values.
1534:      *
1535:      * @param array $hash  Array containing all the values.
1536:      *
1537:      * @throws Kronolith_Exception
1538:      */
1539:     public function fromHash($hash)
1540:     {
1541:         // See if it's a new event.
1542:         if ($this->id === null) {
1543:             $this->creator = $GLOBALS['registry']->getAuth();
1544:         }
1545:         if (!empty($hash['title'])) {
1546:             $this->title = $hash['title'];
1547:         } else {
1548:             throw new Kronolith_Exception(_("Events must have a title."));
1549:         }
1550:         if (!empty($hash['description'])) {
1551:             $this->description = $hash['description'];
1552:         }
1553:         if (!empty($hash['location'])) {
1554:             $this->location = $hash['location'];
1555:         }
1556:         if (!empty($hash['private'])) {
1557:             $this->private = true;
1558:         }
1559:         if (!empty($hash['start_date'])) {
1560:             $date = explode('-', $hash['start_date']);
1561:             if (empty($hash['start_time'])) {
1562:                 $time = array(0, 0, 0);
1563:             } else {
1564:                 $time = explode(':', $hash['start_time']);
1565:                 if (count($time) == 2) {
1566:                     $time[2] = 0;
1567:                 }
1568:             }
1569:             if (count($time) == 3 && count($date) == 3) {
1570:                 $this->start = new Horde_Date(array('year' => $date[0],
1571:                                                     'month' => $date[1],
1572:                                                     'mday' => $date[2],
1573:                                                     'hour' => $time[0],
1574:                                                     'min' => $time[1],
1575:                                                     'sec' => $time[2]));
1576:             }
1577:         } else {
1578:             throw new Kronolith_Exception(_("Events must have a start date."));
1579:         }
1580:         if (empty($hash['duration'])) {
1581:             if (empty($hash['end_date'])) {
1582:                 $hash['end_date'] = $hash['start_date'];
1583:             }
1584:             if (empty($hash['end_time'])) {
1585:                 $hash['end_time'] = $hash['start_time'];
1586:             }
1587:         } else {
1588:             $weeks = str_replace('W', '', $hash['duration'][1]);
1589:             $days = str_replace('D', '', $hash['duration'][2]);
1590:             $hours = str_replace('H', '', $hash['duration'][4]);
1591:             $minutes = isset($hash['duration'][5]) ? str_replace('M', '', $hash['duration'][5]) : 0;
1592:             $seconds = isset($hash['duration'][6]) ? str_replace('S', '', $hash['duration'][6]) : 0;
1593:             $hash['duration'] = ($weeks * 60 * 60 * 24 * 7) + ($days * 60 * 60 * 24) + ($hours * 60 * 60) + ($minutes * 60) + $seconds;
1594:             $this->end = new Horde_Date($this->start);
1595:             $this->end->sec += $hash['duration'];
1596:         }
1597:         if (!empty($hash['end_date'])) {
1598:             $date = explode('-', $hash['end_date']);
1599:             if (empty($hash['end_time'])) {
1600:                 $time = array(0, 0, 0);
1601:             } else {
1602:                 $time = explode(':', $hash['end_time']);
1603:                 if (count($time) == 2) {
1604:                     $time[2] = 0;
1605:                 }
1606:             }
1607:             if (count($time) == 3 && count($date) == 3) {
1608:                 $this->end = new Horde_Date(array('year' => $date[0],
1609:                                                   'month' => $date[1],
1610:                                                   'mday' => $date[2],
1611:                                                   'hour' => $time[0],
1612:                                                   'min' => $time[1],
1613:                                                   'sec' => $time[2]));
1614:             }
1615:         }
1616:         if (!empty($hash['alarm'])) {
1617:             $this->alarm = (int)$hash['alarm'];
1618:         } elseif (!empty($hash['alarm_date']) &&
1619:                   !empty($hash['alarm_time'])) {
1620:             $date = explode('-', $hash['alarm_date']);
1621:             $time = explode(':', $hash['alarm_time']);
1622:             if (count($time) == 2) {
1623:                 $time[2] = 0;
1624:             }
1625:             if (count($time) == 3 && count($date) == 3) {
1626:                 $alarm = new Horde_Date(array('hour'  => $time[0],
1627:                                               'min'   => $time[1],
1628:                                               'sec'   => $time[2],
1629:                                               'month' => $date[1],
1630:                                               'mday'  => $date[2],
1631:                                               'year'  => $date[0]));
1632:                 $this->alarm = ($this->start->timestamp() - $alarm->timestamp()) / 60;
1633:             }
1634:         }
1635:         if (!empty($hash['recur_type'])) {
1636:             $this->recurrence = new Horde_Date_Recurrence($this->start);
1637:             $this->recurrence->setRecurType($hash['recur_type']);
1638:             if (!empty($hash['recur_end_date'])) {
1639:                 $date = explode('-', $hash['recur_end_date']);
1640:                 $this->recurrence->setRecurEnd(new Horde_Date(array('year' => $date[0], 'month' => $date[1], 'mday' => $date[2])));
1641:             }
1642:             if (!empty($hash['recur_interval'])) {
1643:                 $this->recurrence->setRecurInterval($hash['recur_interval']);
1644:             }
1645:             if (!empty($hash['recur_data'])) {
1646:                 $this->recurrence->setRecurOnDay($hash['recur_data']);
1647:             }
1648:         }
1649: 
1650:         $this->initialized = true;
1651:     }
1652: 
1653:     /**
1654:      * Returns an alarm hash of this event suitable for Horde_Alarm.
1655:      *
1656:      * @param Horde_Date $time  Time of alarm.
1657:      * @param string $user      The user to return alarms for.
1658:      * @param Prefs $prefs      A Prefs instance.
1659:      *
1660:      * @return array  Alarm hash or null.
1661:      */
1662:     public function toAlarm($time, $user = null, $prefs = null)
1663:     {
1664:         if (!$this->alarm) {
1665:             return;
1666:         }
1667: 
1668:         if ($this->recurs()) {
1669:             $eventDate = $this->recurrence->nextRecurrence($time);
1670:             if ($eventDate && $this->recurrence->hasException($eventDate->year, $eventDate->month, $eventDate->mday)) {
1671:                 return;
1672:             }
1673:         }
1674: 
1675:         $serverName = $_SERVER['SERVER_NAME'];
1676:         $serverConf = $GLOBALS['conf']['server']['name'];
1677:         if (!empty($GLOBALS['conf']['reminder']['server_name'])) {
1678:             $_SERVER['SERVER_NAME'] = $GLOBALS['conf']['server']['name'] = $GLOBALS['conf']['reminder']['server_name'];
1679:         }
1680: 
1681:         if (empty($user)) {
1682:             $user = $GLOBALS['registry']->getAuth();
1683:         }
1684:         if (empty($prefs)) {
1685:             $prefs = $GLOBALS['prefs'];
1686:         }
1687: 
1688:         $methods = !empty($this->methods) ? $this->methods : @unserialize($prefs->getValue('event_alarms'));
1689:         $start = clone $this->start;
1690:         $start->min -= $this->alarm;
1691:         if (isset($methods['notify'])) {
1692:             $methods['notify']['show'] = array(
1693:                 '__app' => $GLOBALS['registry']->getApp(),
1694:                 'event' => $this->id,
1695:                 'calendar' => $this->calendar);
1696:             $methods['notify']['ajax'] = 'event:' . $this->calendarType . '|' . $this->calendar . ':' . $this->id . ':' . $start->dateString();
1697:             if (!empty($methods['notify']['sound'])) {
1698:                 if ($methods['notify']['sound'] == 'on') {
1699:                     // Handle boolean sound preferences.
1700:                     $methods['notify']['sound'] = (string)Horde_Themes::sound('theetone.wav');
1701:                 } else {
1702:                     // Else we know we have a sound name that can be
1703:                     // served from Horde.
1704:                     $methods['notify']['sound'] = (string)Horde_Themes::sound($methods['notify']['sound']);
1705:                 }
1706:             }
1707:             if ($this->isAllDay()) {
1708:                 if ($this->start->compareDate($this->end) == 0) {
1709:                     $methods['notify']['subtitle'] = sprintf(_("On %s"), '<strong>' . $this->start->strftime($prefs->getValue('date_format')) . '</strong>');
1710:                 } else {
1711:                     $methods['notify']['subtitle'] = sprintf(_("From %s to %s"), '<strong>' . $this->start->strftime($prefs->getValue('date_format')) . '</strong>', '<strong>' . $this->end->strftime($prefs->getValue('date_format')) . '</strong>');
1712:                 }
1713:             } else {
1714:                 $methods['notify']['subtitle'] = sprintf(_("From %s at %s to %s at %s"), '<strong>' . $this->start->strftime($prefs->getValue('date_format')), $this->start->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia') . '</strong>', '<strong>' . $this->end->strftime($prefs->getValue('date_format')), $this->end->format($prefs->getValue('twentyFour') ? 'H:i' : 'h:ia') . '</strong>');
1715:             }
1716:         }
1717:         if (isset($methods['mail'])) {
1718:             $image = Kronolith::getImagePart('big_alarm.png');
1719: 
1720:             $view = new Horde_View(array('templatePath' => KRONOLITH_TEMPLATES . '/alarm', 'encoding' => 'UTF-8'));
1721:             new Horde_View_Helper_Text($view);
1722:             $view->event = $this;
1723:             $view->imageId = $image->getContentId();
1724:             $view->user = $user;
1725:             $view->dateFormat = $prefs->getValue('date_format');
1726:             $view->timeFormat = $prefs->getValue('twentyFour') ? 'H:i' : 'h:ia';
1727:             if (!$prefs->isLocked('event_reminder')) {
1728:                 $view->prefsUrl = Horde::url(Horde::getServiceLink('prefs', 'kronolith'), true)->remove(session_name());
1729:             }
1730:             if (!$this->isPrivate() && $this->attendees) {
1731:                 $attendees = array();
1732:                 foreach ($this->attendees as $mail => $attendee) {
1733:                     $attendees[] = empty($attendee['name']) ? $mail : Horde_Mime_Address::trimAddress($attendee['name'] . (strpos($mail, '@') === false ? '' : ' <' . $mail . '>'));
1734:                 }
1735:                 $view->attendees = $attendees;
1736:             }
1737: 
1738:             $methods['mail']['mimepart'] = Kronolith::buildMimeMessage($view, 'mail', $image);
1739:         }
1740: 
1741:         $alarm = array(
1742:             'id' => $this->uid,
1743:             'user' => $user,
1744:             'start' => $start,
1745:             'end' => $this->end,
1746:             'methods' => array_keys($methods),
1747:             'params' => $methods,
1748:             'title' => $this->getTitle($user),
1749:             'text' => $this->description);
1750: 
1751:         $_SERVER['SERVER_NAME'] = $serverName;
1752:         $GLOBALS['conf']['server']['name'] = $serverConf;
1753: 
1754:         return $alarm;
1755:     }
1756: 
1757:     /**
1758:      * Returns a simple object suitable for json transport representing this
1759:      * event.
1760:      *
1761:      * Possible properties are:
1762:      * - t: title
1763:      * - d: description
1764:      * - c: calendar id
1765:      * - s: start date
1766:      * - e: end date
1767:      * - fi: first day of a multi-day event
1768:      * - la: last day of a multi-day event
1769:      * - x: status (Kronolith::STATUS_* constant)
1770:      * - al: all-day?
1771:      * - bg: background color
1772:      * - fg: foreground color
1773:      * - pe: edit permissions?
1774:      * - pd: delete permissions?
1775:      * - vl: variable, i.e. editable length?
1776:      * - a: alarm text or minutes
1777:      * - r: recurrence type (Horde_Date_Recurrence::RECUR_* constant) or json
1778:      *      representation of Horde_Date_Recurrence object.
1779:      * - bid: The baseid for an event representing an exception
1780:      * - eod: The original date that an exception is replacing
1781:      * - ic: icon
1782:      * - ln: link
1783:      * - aj: ajax link
1784:      * - id: event id
1785:      * - ty: calendar type (driver)
1786:      * - l: location
1787:      * - u: url
1788:      * - sd: formatted start date
1789:      * - st: formatted start time
1790:      * - ed: formatted end date
1791:      * - et: formatted end time
1792:      * - at: attendees
1793:      * - tg: tag list
1794:      *
1795:      * @param boolean $allDay      If not null, overrides whether the event is
1796:      *                             an all-day event.
1797:      * @param boolean $full        Whether to return all event details.
1798:      * @param string $time_format  The date() format to use for time formatting.
1799:      *
1800:      * @return stdClass  A simple object.
1801:      */
1802:     public function toJson($allDay = null, $full = false, $time_format = 'H:i')
1803:     {
1804:         $json = new stdClass;
1805:         $json->t = $this->getTitle();
1806:         $json->c = $this->calendar;
1807:         $json->s = $this->start->toJson();
1808:         $json->e = $this->end->toJson();
1809:         $json->fi = $this->first;
1810:         $json->la = $this->last;
1811:         $json->x = (int)$this->status;
1812:         $json->al = is_null($allDay) ? $this->isAllDay() : $allDay;
1813:         $json->pe = $this->hasPermission(Horde_Perms::EDIT);
1814:         $json->pd = $this->hasPermission(Horde_Perms::DELETE);
1815:         $json->l = $this->getLocation();
1816:         if ($this->icon) {
1817:             $json->ic = $this->icon;
1818:         }
1819:         if ($this->alarm) {
1820:             if ($this->alarm % 10080 == 0) {
1821:                 $alarm_value = $this->alarm / 10080;
1822:                 $json->a = sprintf(ngettext("%d week", "%d weeks", $alarm_value), $alarm_value);
1823:             } elseif ($this->alarm % 1440 == 0) {
1824:                 $alarm_value = $this->alarm / 1440;
1825:                 $json->a = sprintf(ngettext("%d day", "%d days", $alarm_value), $alarm_value);
1826:             } elseif ($this->alarm % 60 == 0) {
1827:                 $alarm_value = $this->alarm / 60;
1828:                 $json->a = sprintf(ngettext("%d hour", "%d hours", $alarm_value), $alarm_value);
1829:             } else {
1830:                 $alarm_value = $this->alarm;
1831:                 $json->a = sprintf(ngettext("%d minute", "%d minutes", $alarm_value), $alarm_value);
1832:             }
1833:         }
1834:         if ($this->recurs()) {
1835:             $json->r = $this->recurrence->getRecurType();
1836:         } elseif ($this->baseid) {
1837:             $json->bid = $this->baseid;
1838:             if ($this->exceptionoriginaldate) {
1839:                 $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')));
1840:             }
1841:         }
1842: 
1843:         if ($full) {
1844:             $json->id = $this->id;
1845:             $json->ty = $this->calendarType;
1846:             $json->sd = $this->start->strftime('%x');
1847:             $json->st = $this->start->format($time_format);
1848:             $json->ed = $this->end->strftime('%x');
1849:             $json->et = $this->end->format($time_format);
1850:             $json->a = $this->alarm;
1851:             $json->pv = $this->private;
1852:             if ($this->recurs()) {
1853:                 $json->r = $this->recurrence->toJson();
1854:             }
1855:             if (!$this->isPrivate()) {
1856:                 $json->d = $this->description;
1857:                 $json->u = $this->url;
1858:                 $json->tg = array_values($this->tags);
1859:                 $json->gl = $this->geoLocation;
1860:                 if ($this->attendees) {
1861:                     $attendees = array();
1862:                     foreach ($this->attendees as $email => $info) {
1863:                         $attendee = array('a' => (int)$info['attendance'],
1864:                                           'r' => (int)$info['response'],
1865:                                           'l' => empty($info['name']) ? $email : Horde_Mime_Address::trimAddress($info['name'] . (strpos($email, '@') === false ? '' : ' <' . $email . '>')));
1866:                         if (strpos($email, '@') !== false) {
1867:                             $attendee['e'] = $email;
1868:                         }
1869:                         $attendees[] = $attendee;
1870:                     }
1871:                     $json->at = $attendees;
1872:                 }
1873:             }
1874:             if ($this->methods) {
1875:                 $json->m = $this->methods;
1876:             }
1877:         }
1878: 
1879:         return $json;
1880:     }
1881: 
1882:     /**
1883:      * Checks if the current event is already present in the calendar.
1884:      *
1885:      * Does the check based on the uid.
1886:      *
1887:      * @return boolean  True if event exists, false otherwise.
1888:      */
1889:     public function exists()
1890:     {
1891:         if (!isset($this->uid) || !isset($this->calendar)) {
1892:             return false;
1893:         }
1894:         try {
1895:             $eventID = $this->getDriver()->exists($this->uid, $this->calendar);
1896:             if (!$eventID) {
1897:                 return false;
1898:             }
1899:         } catch (Exception $e) {
1900:             return false;
1901:         }
1902:         $this->id = $eventID;
1903:         return true;
1904:     }
1905: 
1906:     public function getDuration()
1907:     {
1908:         if (isset($this->_duration)) {
1909:             return $this->_duration;
1910:         }
1911: 
1912:         if ($this->start && $this->end) {
1913:             $dur_day_match = Date_Calc::dateDiff($this->start->mday,
1914:                                                  $this->start->month,
1915:                                                  $this->start->year,
1916:                                                  $this->end->mday,
1917:                                                  $this->end->month,
1918:                                                  $this->end->year);
1919:             $dur_hour_match = $this->end->hour - $this->start->hour;
1920:             $dur_min_match = $this->end->min - $this->start->min;
1921:             while ($dur_min_match < 0) {
1922:                 $dur_min_match += 60;
1923:                 --$dur_hour_match;
1924:             }
1925:             while ($dur_hour_match < 0) {
1926:                 $dur_hour_match += 24;
1927:                 --$dur_day_match;
1928:             }
1929:             if ($dur_hour_match == 0 && $dur_min_match == 0 &&
1930:                 $this->end->mday - $this->start->mday == 1) {
1931:                 $dur_day_match = 1;
1932:                 $dur_hour_match = 0;
1933:                 $dur_min_match = 0;
1934:                 $whole_day_match = true;
1935:             } else {
1936:                 $whole_day_match = false;
1937:             }
1938:         } else {
1939:             $dur_day_match = 0;
1940:             $dur_hour_match = 1;
1941:             $dur_min_match = 0;
1942:             $whole_day_match = false;
1943:         }
1944: 
1945:         $this->_duration = new stdClass;
1946:         $this->_duration->day = $dur_day_match;
1947:         $this->_duration->hour = $dur_hour_match;
1948:         $this->_duration->min = $dur_min_match;
1949:         $this->_duration->wholeDay = $whole_day_match;
1950: 
1951:         return $this->_duration;
1952:     }
1953: 
1954:     /**
1955:      * Returns whether this event is a recurring event.
1956:      *
1957:      * @return boolean  True if this is a recurring event.
1958:      */
1959:     public function recurs()
1960:     {
1961:         return isset($this->recurrence) &&
1962:             !$this->recurrence->hasRecurType(Horde_Date_Recurrence::RECUR_NONE) &&
1963:             empty($this->baseid);
1964:     }
1965: 
1966:     /**
1967:      * Returns a description of this event's recurring type.
1968:      *
1969:      * @return string  Human readable recurring type.
1970:      */
1971:     public function getRecurName()
1972:     {
1973:         if (empty($this->baseid)) {
1974:             return $this->recurs()
1975:                 ? $this->recurrence->getRecurName()
1976:                 : _("No recurrence");
1977:         } else {
1978:             return _("Exception");
1979:         }
1980:     }
1981: 
1982:     /**
1983:      * Returns a correcty formatted exception date for recurring events and a
1984:      * link to delete this exception.
1985:      *
1986:      * @param string $date  Exception in the format Ymd.
1987:      *
1988:      * @return string  The formatted date and delete link.
1989:      */
1990:     public function exceptionLink($date)
1991:     {
1992:         if (!preg_match('/(\d{4})(\d{2})(\d{2})/', $date, $match)) {
1993:             return '';
1994:         }
1995:         $horde_date = new Horde_Date(array('year' => $match[1],
1996:                                            'month' => $match[2],
1997:                                            'mday' => $match[3]));
1998:         $formatted = $horde_date->strftime($GLOBALS['prefs']->getValue('date_format'));
1999:         return $formatted
2000:             . Horde::url('edit.php')
2001:             ->add(array('calendar' => $this->calendarType . '_' .$this->calendar,
2002:                         'eventID' => $this->id,
2003:                         'del_exception' => $date,
2004:                         'url' => Horde_Util::getFormData('url')))
2005:             ->link(array('title' => sprintf(_("Delete exception on %s"), $formatted)))
2006:             . Horde::img('delete-small.png', _("Delete"))
2007:             . '</a>';
2008:     }
2009: 
2010:     /**
2011:      * Returns a list of exception dates for recurring events including links
2012:      * to delete them.
2013:      *
2014:      * @return string  List of exception dates and delete links.
2015:      */
2016:     public function exceptionsList()
2017:     {
2018:         $exceptions = $this->recurrence->getExceptions();
2019:         asort($exceptions);
2020:         return implode(', ', array_map(array($this, 'exceptionLink'), $exceptions));
2021:     }
2022: 
2023:     /**
2024:      * Returns whether the event should be considered private.
2025:      *
2026:      * The event's private flag can be overriden if the current user
2027:      * is an administrator and the code is run from command line. This
2028:      * is to allow full event notifications in alarm messages or
2029:      * agendas.
2030:      *
2031:      * @param string $user  The current user.
2032:      *
2033:      * @return boolean  Whether to consider the event as private.
2034:      */
2035:     public function isPrivate($user = null)
2036:     {
2037:         if ($user === null) {
2038:             $user = $GLOBALS['registry']->getAuth();
2039:         }
2040: 
2041:         if (!(Horde_Cli::runningFromCLI() && $GLOBALS['registry']->isAdmin()) &&
2042:             $this->private && $this->creator != $user) {
2043:             return true;
2044:         }
2045:         if ($GLOBALS['registry']->isAdmin() ||
2046:             $this->hasPermission(Horde_Perms::READ, $user)) {
2047:             return false;
2048:         }
2049:         return true;
2050:     }
2051: 
2052:     /**
2053:      * Returns the title of this event, considering private flags.
2054:      *
2055:      * @param string $user  The current user.
2056:      *
2057:      * @return string  The title of this event.
2058:      */
2059:     public function getTitle($user = null)
2060:     {
2061:         if (!$this->initialized) {
2062:             return '';
2063:         }
2064: 
2065:         return $this->isPrivate($user)
2066:             ? _("busy")
2067:             : (strlen($this->title) ? $this->title : _("[Unnamed event]"));
2068:     }
2069: 
2070:     /**
2071:      * Returns the location of this event, considering private flags.
2072:      *
2073:      * @param string $user  The current user.
2074:      *
2075:      * @return string  The location of this event.
2076:      */
2077:     public function getLocation($user = null)
2078:     {
2079:         return $this->isPrivate($user) ? '' : $this->location;
2080:     }
2081: 
2082:     /**
2083:      * Checks to see whether the specified attendee is associated with the
2084:      * current event.
2085:      *
2086:      * @param string $email  The email address of the attendee.
2087:      *
2088:      * @return boolean  True if the specified attendee is present for this
2089:      *                  event.
2090:      */
2091:     public function hasAttendee($email)
2092:     {
2093:         return isset($this->attendees[Horde_String::lower($email)]);
2094:     }
2095: 
2096:     /**
2097:      * Adds a new attendee to the current event.
2098:      *
2099:      * This will overwrite an existing attendee if one exists with the same
2100:      * email address.
2101:      *
2102:      * @param string $email        The email address of the attendee.
2103:      * @param integer $attendance  The attendance code of the attendee.
2104:      * @param integer $response    The response code of the attendee.
2105:      * @param string $name         The name of the attendee.
2106:      */
2107:     public function addAttendee($email, $attendance, $response, $name = null)
2108:     {
2109:         $email = Horde_String::lower($email);
2110:         if ($attendance == Kronolith::PART_IGNORE) {
2111:             if (isset($this->attendees[$email])) {
2112:                 $attendance = $this->attendees[$email]['attendance'];
2113:             } else {
2114:                 $attendance = Kronolith::PART_REQUIRED;
2115:             }
2116:         }
2117:         if (empty($name) && isset($this->attendees[$email]) &&
2118:             !empty($this->attendees[$email]['name'])) {
2119:             $name = $this->attendees[$email]['name'];
2120:         }
2121: 
2122:         $this->attendees[$email] = array(
2123:             'attendance' => $attendance,
2124:             'response' => $response,
2125:             'name' => $name
2126:         );
2127:     }
2128: 
2129:     /**
2130:      * Adds a single resource to this event.
2131:      *
2132:      * No validation or acceptence/denial is done here...it should be done
2133:      * when saving the event.
2134:      *
2135:      * @param Kronolith_Resource $resource  The resource to add.
2136:      */
2137:     public function addResource($resource, $response)
2138:     {
2139:         $this->_resources[$resource->getId()] = array(
2140:             'attendance' => Kronolith::PART_REQUIRED,
2141:             'response' => $response,
2142:             'name' => $resource->get('name')
2143:         );
2144:     }
2145: 
2146:     /**
2147:      * Removes a resource from this event.
2148:      *
2149:      * @param Kronolith_Resource $resource  The resource to remove.
2150:      */
2151:     public function removeResource($resource)
2152:     {
2153:         if (isset($this->_resources[$resource->getId()])) {
2154:             unset($this->_resources[$resource->getId()]);
2155:         }
2156:     }
2157: 
2158:     /**
2159:      * Returns all resources.
2160:      *
2161:      * @return array  A copy of the resources array.
2162:      */
2163:     public function getResources()
2164:     {
2165:         return $this->_resources;
2166:     }
2167: 
2168:     public function isAllDay()
2169:     {
2170:         return $this->allday ||
2171:             ($this->start->hour == 0 && $this->start->min == 0 && $this->start->sec == 0 &&
2172:              (($this->end->hour == 23 && $this->end->min == 59) ||
2173:               ($this->end->hour == 0 && $this->end->min == 0 && $this->end->sec == 0 &&
2174:                ($this->end->mday > $this->start->mday ||
2175:                 $this->end->month > $this->start->month ||
2176:                 $this->end->year > $this->start->year))));
2177:     }
2178: 
2179:     public function readForm()
2180:     {
2181:         global $prefs, $cManager, $session;
2182: 
2183:         // Event owner.
2184:         $targetcalendar = Horde_Util::getFormData('targetcalendar');
2185:         if (strpos($targetcalendar, '\\')) {
2186:             list(, $this->creator) = explode('\\', $targetcalendar, 2);
2187:         } elseif (!isset($this->_id)) {
2188:             $this->creator = $GLOBALS['registry']->getAuth();
2189:         }
2190: 
2191:         // Basic fields.
2192:         $this->title = Horde_Util::getFormData('title', $this->title);
2193:         $this->description = Horde_Util::getFormData('description', $this->description);
2194:         $this->location = Horde_Util::getFormData('location', $this->location);
2195:         $this->private = (bool)Horde_Util::getFormData('private');
2196: 
2197:         // URL.
2198:         $url = Horde_Util::getFormData('eventurl', $this->url);
2199:         if (strlen($url)) {
2200:             // Analyze and re-construct.
2201:             $url = @parse_url($url);
2202:             if ($url) {
2203:                 if (function_exists('http_build_url')) {
2204:                     if (empty($url['path'])) {
2205:                         $url['path'] = '/';
2206:                     }
2207:                     $url = http_build_url($url);
2208:                 } else {
2209:                     $new_url = '';
2210:                     if (isset($url['scheme'])) {
2211:                         $new_url .= $url['scheme'] . '://';
2212:                     }
2213:                     if (isset($url['user'])) {
2214:                         $new_url .= $url['user'];
2215:                         if (isset($url['pass'])) {
2216:                             $new_url .= ':' . $url['pass'];
2217:                         }
2218:                         $new_url .= '@';
2219:                     }
2220:                     if (isset($url['host'])) {
2221:                         // Convert IDN hosts to ASCII.
2222:                         if (function_exists('idn_to_ascii')) {
2223:                             $url['host'] = @idn_to_ascii($url['host']);
2224:                         } elseif (Horde_Mime::is8bit($url['host'])) {
2225:                             //throw new Kronolith_Exception(_("Invalid character in URL."));
2226:                             $url['host'] = '';
2227:                         }
2228:                         $new_url .= $url['host'];
2229:                     }
2230:                     if (isset($url['path'])) {
2231:                         $new_url .= $url['path'];
2232:                     }
2233:                     if (isset($url['query'])) {
2234:                         $new_url .= '?' . $url['query'];
2235:                     }
2236:                     if (isset($url['fragment'])) {
2237:                         $new_url .= '#' . $url['fragment'];
2238:                     }
2239:                     $url = $new_url;
2240:                 }
2241:             }
2242:         }
2243:         $this->url = $url;
2244: 
2245:         // Status.
2246:         $this->status = Horde_Util::getFormData('status', $this->status);
2247: 
2248:         // Attendees.
2249:         $attendees = $session->get('kronolith', 'attendees', Horde_Session::TYPE_ARRAY);
2250:         if (!is_null($newattendees = Horde_Util::getFormData('attendees'))) {
2251:             $newattendees = Kronolith::parseAttendees(trim($newattendees));
2252:             foreach ($newattendees as $email => $attendee) {
2253:                 if (!isset($attendees[$email])) {
2254:                     $attendees[$email] = $attendee;
2255:                 }
2256:             }
2257:             foreach (array_keys($attendees) as $email) {
2258:                 if (!isset($newattendees[$email])) {
2259:                     unset($attendees[$email]);
2260:                 }
2261:             }
2262:         }
2263:         $this->attendees = $attendees;
2264: 
2265:         // Resources
2266:         $this->_resources = $session->get('kronolith', 'resources', Horde_Session::TYPE_ARRAY);
2267: 
2268:         // Event start.
2269:         $allDay = Horde_Util::getFormData('whole_day');
2270:         if ($start_date = Horde_Util::getFormData('start_date')) {
2271:             // From ajax interface.
2272:             $this->start = Kronolith::parseDate($start_date . ' ' . Horde_Util::getFormData('start_time'));
2273:             if ($allDay) {
2274:                 $this->start->hour = $this->start->min = $this->start->sec = 0;
2275:             }
2276:         } else {
2277:             // From traditional interface.
2278:             $start = Horde_Util::getFormData('start');
2279:             $start_year = $start['year'];
2280:             $start_month = $start['month'];
2281:             $start_day = $start['day'];
2282:             $start_hour = Horde_Util::getFormData('start_hour');
2283:             $start_min = Horde_Util::getFormData('start_min');
2284:             $am_pm = Horde_Util::getFormData('am_pm');
2285: 
2286:             if (!$prefs->getValue('twentyFour')) {
2287:                 if ($am_pm == 'PM') {
2288:                     if ($start_hour != 12) {
2289:                         $start_hour += 12;
2290:                     }
2291:                 } elseif ($start_hour == 12) {
2292:                     $start_hour = 0;
2293:                 }
2294:             }
2295: 
2296:             if (Horde_Util::getFormData('end_or_dur') == 1) {
2297:                 if ($allDay) {
2298:                     $start_hour = 0;
2299:                     $start_min = 0;
2300:                     $dur_day = 0;
2301:                     $dur_hour = 24;
2302:                     $dur_min = 0;
2303:                 } else {
2304:                     $dur_day = (int)Horde_Util::getFormData('dur_day');
2305:                     $dur_hour = (int)Horde_Util::getFormData('dur_hour');
2306:                     $dur_min = (int)Horde_Util::getFormData('dur_min');
2307:                 }
2308:             }
2309: 
2310:             $this->start = new Horde_Date(array('hour' => $start_hour,
2311:                                                 'min' => $start_min,
2312:                                                 'month' => $start_month,
2313:                                                 'mday' => $start_day,
2314:                                                 'year' => $start_year));
2315:         }
2316: 
2317:         // Event end.
2318:         if ($end_date = Horde_Util::getFormData('end_date')) {
2319:             // From ajax interface.
2320:             $this->end = Kronolith::parseDate($end_date . ' ' . Horde_Util::getFormData('end_time'));
2321:             if ($allDay) {
2322:                 $this->end->hour = 23;
2323:                 $this->end->min = $this->end->sec = 59;
2324:             }
2325:         } elseif (Horde_Util::getFormData('end_or_dur') == 1) {
2326:             // Event duration from traditional interface.
2327:             $this->end = new Horde_Date(array('hour' => $start_hour + $dur_hour,
2328:                                               'min' => $start_min + $dur_min,
2329:                                               'month' => $start_month,
2330:                                               'mday' => $start_day + $dur_day,
2331:                                               'year' => $start_year));
2332:         } else {
2333:             // From traditional interface.
2334:             $end = Horde_Util::getFormData('end');
2335:             $end_year = $end['year'];
2336:             $end_month = $end['month'];
2337:             $end_day = $end['day'];
2338:             $end_hour = Horde_Util::getFormData('end_hour');
2339:             $end_min = Horde_Util::getFormData('end_min');
2340:             $end_am_pm = Horde_Util::getFormData('end_am_pm');
2341: 
2342:             if (!$prefs->getValue('twentyFour')) {
2343:                 if ($end_am_pm == 'PM') {
2344:                     if ($end_hour != 12) {
2345:                         $end_hour += 12;
2346:                     }
2347:                 } elseif ($end_hour == 12) {
2348:                     $end_hour = 0;
2349:                 }
2350:             }
2351: 
2352:             $this->end = new Horde_Date(array('hour' => $end_hour,
2353:                                               'min' => $end_min,
2354:                                               'month' => $end_month,
2355:                                               'mday' => $end_day,
2356:                                               'year' => $end_year));
2357:             if ($this->end->compareDateTime($this->start) < 0) {
2358:                 $this->end = new Horde_Date($this->start);
2359:             }
2360:         }
2361: 
2362:         $this->allday = false;
2363: 
2364:         // Alarm.
2365:         if (!is_null($alarm = Horde_Util::getFormData('alarm'))) {
2366:             if ($alarm) {
2367:                 $value = Horde_Util::getFormData('alarm_value');
2368:                 $unit = Horde_Util::getFormData('alarm_unit');
2369:                 if ($value == 0) {
2370:                     $value = $unit = 1;
2371:                 }
2372:                 $this->alarm = $value * $unit;
2373:                 // Notification.
2374:                 if (Horde_Util::getFormData('alarm_change_method')) {
2375:                     $types = Horde_Util::getFormData('event_alarms');
2376:                     if (!empty($types)) {
2377:                         $methods = array();
2378:                         foreach ($types as $type) {
2379:                             $methods[$type] = array();
2380:                             switch ($type){
2381:                             case 'notify':
2382:                                 $methods[$type]['sound'] = Horde_Util::getFormData('event_alarms_sound');
2383:                                 break;
2384:                             case 'mail':
2385:                                 $methods[$type]['email'] = Horde_Util::getFormData('event_alarms_email');
2386:                                 break;
2387:                             case 'popup':
2388:                                 break;
2389:                             }
2390:                         }
2391:                         $this->methods = $methods;
2392:                     }
2393:                 } else {
2394:                     $this->methods = array();
2395:                 }
2396:             } else {
2397:                 $this->alarm = 0;
2398:                 $this->methods = array();
2399:             }
2400:         }
2401: 
2402:         // Recurrence.
2403:         $recur = Horde_Util::getFormData('recur');
2404:         if ($recur !== null && $recur !== '') {
2405:             if (!isset($this->recurrence)) {
2406:                 $this->recurrence = new Horde_Date_Recurrence($this->start);
2407:             } else {
2408:                 $this->recurrence->setRecurStart($this->start);
2409:             }
2410:             if (Horde_Util::getFormData('recur_end_type') == 'date') {
2411:                 if ($end_date = Horde_Util::getFormData('recur_end_date')) {
2412:                     // From ajax interface.
2413:                     $date_ob = Kronolith::parseDate($end_date, false);
2414:                     $recur_enddate = array('year'  => $date_ob->year,
2415:                                            'month' => $date_ob->month,
2416:                                            'day'  => $date_ob->mday);
2417:                 } else {
2418:                     // From traditional interface.
2419:                     $recur_enddate = Horde_Util::getFormData('recur_end');
2420:                 }
2421:                 if ($this->recurrence->hasRecurEnd()) {
2422:                     $recurEnd = $this->recurrence->recurEnd;
2423:                     $recurEnd->month = $recur_enddate['month'];
2424:                     $recurEnd->mday = $recur_enddate['day'];
2425:                     $recurEnd->year = $recur_enddate['year'];
2426:                 } else {
2427:                     $recurEnd = new Horde_Date(
2428:                         array('hour' => 23,
2429:                               'min' => 59,
2430:                               'sec' => 59,
2431:                               'month' => $recur_enddate['month'],
2432:                               'mday' => $recur_enddate['day'],
2433:                               'year' => $recur_enddate['year']));
2434:                 }
2435:                 $this->recurrence->setRecurEnd($recurEnd);
2436:             } elseif (Horde_Util::getFormData('recur_end_type') == 'count') {
2437:                 $this->recurrence->setRecurCount(Horde_Util::getFormData('recur_count'));
2438:             } elseif (Horde_Util::getFormData('recur_end_type') == 'none') {
2439:                 $this->recurrence->setRecurCount(0);
2440:                 $this->recurrence->setRecurEnd(null);
2441:             }
2442: 
2443:             $this->recurrence->setRecurType($recur);
2444:             switch ($recur) {
2445:             case Horde_Date_Recurrence::RECUR_DAILY:
2446:                 $this->recurrence->setRecurInterval(Horde_Util::getFormData('recur_daily_interval', 1));
2447:                 break;
2448: 
2449:             case Horde_Date_Recurrence::RECUR_WEEKLY:
2450:                 $weekly = Horde_Util::getFormData('weekly');
2451:                 $weekdays = 0;
2452:                 if (is_array($weekly)) {
2453:                     foreach ($weekly as $day) {
2454:                         $weekdays |= $day;
2455:                     }
2456:                 }
2457: 
2458:                 if ($weekdays == 0) {
2459:                     // Sunday starts at 0.
2460:                     switch ($this->start->dayOfWeek()) {
2461:                     case 0: $weekdays |= Horde_Date::MASK_SUNDAY; break;
2462:                     case 1: $weekdays |= Horde_Date::MASK_MONDAY; break;
2463:                     case 2: $weekdays |= Horde_Date::MASK_TUESDAY; break;
2464:                     case 3: $weekdays |= Horde_Date::MASK_WEDNESDAY; break;
2465:                     case 4: $weekdays |= Horde_Date::MASK_THURSDAY; break;
2466:                     case 5: $weekdays |= Horde_Date::MASK_FRIDAY; break;
2467:                     case 6: $weekdays |= Horde_Date::MASK_SATURDAY; break;
2468:                     }
2469:                 }
2470: 
2471:                 $this->recurrence->setRecurInterval(Horde_Util::getFormData('recur_weekly_interval', 1));
2472:                 $this->recurrence->setRecurOnDay($weekdays);
2473:                 break;
2474: 
2475:             case Horde_Date_Recurrence::RECUR_MONTHLY_DATE:
2476:                 switch (Horde_Util::getFormData('recur_monthly_scheme')) {
2477:                 case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY:
2478:                     $this->recurrence->setRecurType(Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY);
2479:                 case Horde_Date_Recurrence::RECUR_MONTHLY_DATE:
2480:                     $this->recurrence->setRecurInterval(Horde_Util::getFormData('recur_monthly') ? 1 : Horde_Util::getFormData('recur_monthly_interval', 1));
2481:                     break;
2482:                 default:
2483:                     $this->recurrence->setRecurInterval(Horde_Util::getFormData('recur_day_of_month_interval', 1));
2484:                     break;
2485:                 }
2486:                 break;
2487: 
2488:             case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY:
2489:                 $this->recurrence->setRecurInterval(Horde_Util::getFormData('recur_week_of_month_interval', 1));
2490:                 break;
2491: 
2492:             case Horde_Date_Recurrence::RECUR_YEARLY_DATE:
2493:                 switch (Horde_Util::getFormData('recur_yearly_scheme')) {
2494:                 case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY:
2495:                 case Horde_Date_Recurrence::RECUR_YEARLY_DAY:
2496:                     $this->recurrence->setRecurType(Horde_Util::getFormData('recur_yearly_scheme'));
2497:                 case Horde_Date_Recurrence::RECUR_YEARLY_DATE:
2498:                     $this->recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly') ? 1 : Horde_Util::getFormData('recur_yearly_interval', 1));
2499:                     break;
2500:                 default:
2501:                     $this->recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly_interval', 1));
2502:                     break;
2503:                 }
2504:                 break;
2505: 
2506:             case Horde_Date_Recurrence::RECUR_YEARLY_DAY:
2507:                 $this->recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly_day_interval', $yearly_interval));
2508:                 break;
2509: 
2510:             case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY:
2511:                 $this->recurrence->setRecurInterval(Horde_Util::getFormData('recur_yearly_weekday_interval', $yearly_interval));
2512:                 break;
2513:             }
2514: 
2515:             if ($exceptions = Horde_Util::getFormData('exceptions')) {
2516:                 foreach ($exceptions as $exception) {
2517:                     $this->recurrence->addException((int)substr($exception, 0, 4),
2518:                                                     (int)substr($exception, 4, 2),
2519:                                                     (int)substr($exception, 6, 2));
2520:                 }
2521:             }
2522:         }
2523: 
2524:         // Tags.
2525:         $this->tags = Horde_Util::getFormData('tags', $this->tags);
2526: 
2527:         // Geolocation
2528:         if (Horde_Util::getFormData('lat') && Horde_Util::getFormData('lon')) {
2529:             $this->geoLocation = array('lat' => Horde_Util::getFormData('lat'),
2530:                                        'lon' => Horde_Util::getFormData('lon'),
2531:                                        'zoom' => Horde_Util::getFormData('zoom'));
2532:         }
2533: 
2534:         $this->initialized = true;
2535:     }
2536: 
2537:     public function html($property)
2538:     {
2539:         global $prefs;
2540: 
2541:         $options = array();
2542:         $attributes = '';
2543:         $sel = false;
2544:         $label = '';
2545: 
2546:         switch ($property) {
2547:         case 'start[year]':
2548:             return  '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . _("Start Year") . '</label>' .
2549:                 '<input name="' . $property . '" value="' . $this->start->year .
2550:                 '" type="text"' .
2551:                 ' id="' . $this->_formIDEncode($property) . '" size="4" maxlength="4" />';
2552: 
2553:         case 'start[month]':
2554:             $sel = $this->start->month;
2555:             for ($i = 1; $i < 13; ++$i) {
2556:                 $options[$i] = strftime('%b', mktime(1, 1, 1, $i, 1));
2557:             }
2558:             $label = _("Start Month");
2559:             break;
2560: 
2561:         case 'start[day]':
2562:             $sel = $this->start->mday;
2563:             for ($i = 1; $i < 32; ++$i) {
2564:                 $options[$i] = $i;
2565:             }
2566:             $label = _("Start Day");
2567:             break;
2568: 
2569:         case 'start_hour':
2570:             $sel = $this->start->format($prefs->getValue('twentyFour') ? 'G' : 'g');
2571:             $hour_min = $prefs->getValue('twentyFour') ? 0 : 1;
2572:             $hour_max = $prefs->getValue('twentyFour') ? 24 : 13;
2573:             for ($i = $hour_min; $i < $hour_max; ++$i) {
2574:                 $options[$i] = $i;
2575:             }
2576:             $label = _("Start Hour");
2577:             break;
2578: 
2579:         case 'start_min':
2580:             $sel = sprintf('%02d', $this->start->min);
2581:             for ($i = 0; $i < 12; ++$i) {
2582:                 $min = sprintf('%02d', $i * 5);
2583:                 $options[$min] = $min;
2584:             }
2585:             $label = _("Start Minute");
2586:             break;
2587: 
2588:         case 'end[year]':
2589:             return  '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . _("End Year") . '</label>' .
2590:                 '<input name="' . $property . '" value="' . $this->end->year .
2591:                 '" type="text"' .
2592:                 ' id="' . $this->_formIDEncode($property) . '" size="4" maxlength="4" />';
2593: 
2594:         case 'end[month]':
2595:             $sel = $this->end ? $this->end->month : $this->start->month;
2596:             for ($i = 1; $i < 13; ++$i) {
2597:                 $options[$i] = strftime('%b', mktime(1, 1, 1, $i, 1));
2598:             }
2599:             $label = _("End Month");
2600:             break;
2601: 
2602:         case 'end[day]':
2603:             $sel = $this->end ? $this->end->mday : $this->start->mday;
2604:             for ($i = 1; $i < 32; ++$i) {
2605:                 $options[$i] = $i;
2606:             }
2607:             $label = _("End Day");
2608:             break;
2609: 
2610:         case 'end_hour':
2611:             $sel = $this->end
2612:                 ? $this->end->format($prefs->getValue('twentyFour') ? 'G' : 'g')
2613:                 : $this->start->format($prefs->getValue('twentyFour') ? 'G' : 'g') + 1;
2614:             $hour_min = $prefs->getValue('twentyFour') ? 0 : 1;
2615:             $hour_max = $prefs->getValue('twentyFour') ? 24 : 13;
2616:             for ($i = $hour_min; $i < $hour_max; ++$i) {
2617:                 $options[$i] = $i;
2618:             }
2619:             $label = _("End Hour");
2620:             break;
2621: 
2622:         case 'end_min':
2623:             $sel = $this->end ? $this->end->min : $this->start->min;
2624:             $sel = sprintf('%02d', $sel);
2625:             for ($i = 0; $i < 12; ++$i) {
2626:                 $min = sprintf('%02d', $i * 5);
2627:                 $options[$min] = $min;
2628:             }
2629:             $label = _("End Minute");
2630:             break;
2631: 
2632:         case 'dur_day':
2633:             $dur = $this->getDuration();
2634:             return  '<label for="' . $property . '" class="hidden">' . _("Duration Day") . '</label>' .
2635:                 '<input name="' . $property . '" value="' . $dur->day .
2636:                 '" type="text"' .
2637:                 ' id="' . $property . '" size="4" maxlength="4" />';
2638: 
2639:         case 'dur_hour':
2640:             $dur = $this->getDuration();
2641:             $sel = $dur->hour;
2642:             for ($i = 0; $i < 24; ++$i) {
2643:                 $options[$i] = $i;
2644:             }
2645:             $label = _("Duration Hour");
2646:             break;
2647: 
2648:         case 'dur_min':
2649:             $dur = $this->getDuration();
2650:             $sel = $dur->min;
2651:             for ($i = 0; $i < 13; ++$i) {
2652:                 $min = sprintf('%02d', $i * 5);
2653:                 $options[$min] = $min;
2654:             }
2655:             $label = _("Duration Minute");
2656:             break;
2657: 
2658:         case 'recur_end[year]':
2659:             if ($this->end) {
2660:                 $end = ($this->recurs() && $this->recurrence->hasRecurEnd())
2661:                         ? $this->recurrence->recurEnd->year
2662:                         : $this->end->year;
2663:             } else {
2664:                 $end = $this->start->year;
2665:             }
2666:             return  '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . _("Recurrence End Year") . '</label>' .
2667:                 '<input name="' . $property . '" value="' . $end .
2668:                 '" type="text"' .
2669:                 ' id="' . $this->_formIDEncode($property) . '" size="4" maxlength="4" />';
2670: 
2671:         case 'recur_end[month]':
2672:             if ($this->end) {
2673:                 $sel = ($this->recurs() && $this->recurrence->hasRecurEnd())
2674:                     ? $this->recurrence->recurEnd->month
2675:                     : $this->end->month;
2676:             } else {
2677:                 $sel = $this->start->month;
2678:             }
2679:             for ($i = 1; $i < 13; ++$i) {
2680:                 $options[$i] = strftime('%b', mktime(1, 1, 1, $i, 1));
2681:             }
2682:             $label = _("Recurrence End Month");
2683:             break;
2684: 
2685:         case 'recur_end[day]':
2686:             if ($this->end) {
2687:                 $sel = ($this->recurs() && $this->recurrence->hasRecurEnd())
2688:                     ? $this->recurrence->recurEnd->mday
2689:                     : $this->end->mday;
2690:             } else {
2691:                 $sel = $this->start->mday;
2692:             }
2693:             for ($i = 1; $i < 32; ++$i) {
2694:                 $options[$i] = $i;
2695:             }
2696:             $label = _("Recurrence End Day");
2697:             break;
2698:         }
2699: 
2700:         if (!$this->_varRenderer) {
2701:             $this->_varRenderer = Horde_Core_Ui_VarRenderer::factory('Html');
2702:         }
2703: 
2704:         return '<label for="' . $this->_formIDEncode($property) . '" class="hidden">' . $label . '</label>' .
2705:             '<select name="' . $property . '"' . $attributes . ' id="' . $this->_formIDEncode($property) . '">' .
2706:             $this->_varRenderer->selectOptions($options, $sel) .
2707:             '</select>';
2708:     }
2709: 
2710:     /**
2711:      * @param array $params
2712:      *
2713:      * @return Horde_Url
2714:      */
2715:     public function getViewUrl($params = array(), $full = false, $encoded = true)
2716:     {
2717:         $params['eventID'] = $this->id;
2718:         $params['calendar'] = $this->calendar;
2719:         $params['type'] = $this->calendarType;
2720: 
2721:         return Horde::url('event.php', $full)->setRaw(!$encoded)->add($params);
2722:     }
2723: 
2724:     /**
2725:      * @param array $params
2726:      *
2727:      * @return Horde_Url
2728:      */
2729:     public function getEditUrl($params = array(), $full = false)
2730:     {
2731:         $params['view'] = 'EditEvent';
2732:         $params['eventID'] = $this->id;
2733:         $params['calendar'] = $this->calendar;
2734:         $params['type'] = $this->calendarType;
2735: 
2736:         return Horde::url('event.php', $full)->add($params);
2737:     }
2738: 
2739:     /**
2740:      * @param array $params
2741:      *
2742:      * @return Horde_Url
2743:      */
2744:     public function getDeleteUrl($params = array(), $full = false)
2745:     {
2746:         $params['view'] = 'DeleteEvent';
2747:         $params['eventID'] = $this->id;
2748:         $params['calendar'] = $this->calendar;
2749:         $params['type'] = $this->calendarType;
2750: 
2751:         return Horde::url('event.php', $full)->add($params);
2752:     }
2753: 
2754:     /**
2755:      * @param array $params
2756:      *
2757:      * @return Horde_Url
2758:      */
2759:     public function getExportUrl($params = array(), $full = false)
2760:     {
2761:         $params['view'] = 'ExportEvent';
2762:         $params['eventID'] = $this->id;
2763:         $params['calendar'] = $this->calendar;
2764:         $params['type'] = $this->calendarType;
2765: 
2766:         return Horde::url('event.php', $full)->add($params);
2767:     }
2768: 
2769:     public function getLink($datetime = null, $icons = true, $from_url = null,
2770:                             $full = false, $encoded = true)
2771:     {
2772:         global $prefs, $registry;
2773: 
2774:         if (is_null($datetime)) {
2775:             $datetime = $this->start;
2776:         }
2777:         if (is_null($from_url)) {
2778:             $from_url = Horde::selfUrl(true, false, true);
2779:         }
2780: 
2781:         $event_title = $this->getTitle();
2782:         $view_url = $this->getViewUrl(array('datetime' => $datetime->strftime('%Y%m%d%H%M%S'), 'url' => $from_url), $full, $encoded);
2783:         $read_permission = $this->hasPermission(Horde_Perms::READ);
2784: 
2785:         $link = '<span' . $this->getCSSColors() . '>';
2786:         if ($read_permission && $view_url) {
2787:             $link .= Horde::linkTooltip($view_url,
2788:                                        $event_title,
2789:                                        $this->getStatusClass(),
2790:                                        '',
2791:                                        '',
2792:                                        $this->getTooltip(),
2793:                                        '',
2794:                                        array('style' => $this->getCSSColors(false)));
2795:         }
2796:         $link .= htmlspecialchars($event_title);
2797:         if ($read_permission && $view_url) {
2798:             $link .= '</a>';
2799:         }
2800: 
2801:         if ($icons && $prefs->getValue('show_icons')) {
2802:             $icon_color = $this->_foregroundColor == '#000' ? '000' : 'fff';
2803:             $status = '';
2804:             if ($this->alarm) {
2805:                 if ($this->alarm % 10080 == 0) {
2806:                     $alarm_value = $this->alarm / 10080;
2807:                     $title = sprintf(ngettext("Alarm %d week before", "Alarm %d weeks before", $alarm_value), $alarm_value);
2808:                 } elseif ($this->alarm % 1440 == 0) {
2809:                     $alarm_value = $this->alarm / 1440;
2810:                     $title = sprintf(ngettext("Alarm %d day before", "Alarm %d days before", $alarm_value), $alarm_value);
2811:                 } elseif ($this->alarm % 60 == 0) {
2812:                     $alarm_value = $this->alarm / 60;
2813:                     $title = sprintf(ngettext("Alarm %d hour before", "Alarm %d hours before", $alarm_value), $alarm_value);
2814:                 } else {
2815:                     $alarm_value = $this->alarm;
2816:                     $title = sprintf(ngettext("Alarm %d minute before", "Alarm %d minutes before", $alarm_value), $alarm_value);
2817:                 }
2818:                 $status .= Horde::fullSrcImg('alarm-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconAlarm')));
2819:             }
2820: 
2821:             if ($this->recurs()) {
2822:                 $title = Kronolith::recurToString($this->recurrence->getRecurType());
2823:                 $status .= Horde::fullSrcImg('recur-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconRecur')));
2824:             } elseif ($this->baseid) {
2825:                 $title = _("Exception");
2826:                 $status .= Horde::fullSrcImg('exception-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconRecur')));
2827:             }
2828: 
2829:             if ($this->private) {
2830:                 $title = _("Private event");
2831:                 $status .= Horde::fullSrcImg('private-' . $icon_color . '.png', array('attr' => array('alt' => $title, 'title' => $title, 'class' => 'iconPrivate')));
2832:             }
2833: 
2834:             if (!empty($this->attendees)) {
2835:                 $status .= Horde::fullSrcImg('attendees-' . $icon_color . '.png', array('attr' => array('alt' => _("Meeting"), 'title' => _("Meeting"), 'class' => 'iconPeople')));
2836:             }
2837: 
2838:             if (!empty($this->icon)) {
2839:                 $link = $status . '<img src="' . $this->icon . '" /> ' . $link;
2840:             } elseif (!empty($status)) {
2841:                 $link .= ' ' . $status;
2842:             }
2843: 
2844:             if ((!$this->private ||
2845:                  $this->creator == $GLOBALS['registry']->getAuth()) &&
2846:                 Kronolith::getDefaultCalendar(Horde_Perms::EDIT)) {
2847:                 $url = $this->getEditUrl(
2848:                     array('datetime' => $datetime->strftime('%Y%m%d%H%M%S'),
2849:                           'url' => $from_url),
2850:                     $full);
2851:                 if ($url) {
2852:                     $link .= $url->link(array('title' => sprintf(_("Edit %s"), $event_title),
2853:                                               'class' => 'iconEdit'))
2854:                         . Horde::fullSrcImg('edit-' . $icon_color . '.png',
2855:                                             array('attr' => array('alt' => _("Edit"))))
2856:                         . '</a>';
2857:                 }
2858:             }
2859:             if ($this->hasPermission(Horde_Perms::DELETE)) {
2860:                 $url = $this->getDeleteUrl(
2861:                     array('datetime' => $datetime->strftime('%Y%m%d%H%M%S'),
2862:                           'url' => $from_url),
2863:                     $full);
2864:                 if ($url) {
2865:                     $link .= $url->link(array('title' => sprintf(_("Delete %s"), $event_title),
2866:                                               'class' => 'iconDelete'))
2867:                         . Horde::fullSrcImg('delete-' . $icon_color . '.png',
2868:                                             array('attr' => array('alt' => _("Delete"))))
2869:                         . '</a>';
2870:                 }
2871:             }
2872:         }
2873: 
2874:         return $link . '</span>';
2875:     }
2876: 
2877:     /**
2878:      * Returns the CSS color definition for this event.
2879:      *
2880:      * @param boolean $with_attribute  Whether to wrap the colors inside a
2881:      *                                 "style" attribute.
2882:      *
2883:      * @return string  A CSS string with color definitions.
2884:      */
2885:     public function getCSSColors($with_attribute = true)
2886:     {
2887:         $css = 'background-color:' . $this->_backgroundColor . ';color:' . $this->_foregroundColor;
2888:         if ($with_attribute) {
2889:             $css = ' style="' . $css . '"';
2890:         }
2891:         return $css;
2892:     }
2893: 
2894:     /**
2895:      * @return string  A tooltip for quick descriptions of this event.
2896:      */
2897:     public function getTooltip()
2898:     {
2899:         $tooltip = $this->getTimeRange()
2900:             . "\n" . sprintf(_("Owner: %s"), ($this->creator == $GLOBALS['registry']->getAuth() ?
2901:                                               _("Me") : Kronolith::getUserName($this->creator)));
2902: 
2903:         if (!$this->isPrivate()) {
2904:             if ($this->location) {
2905:                 $tooltip .= "\n" . _("Location") . ': ' . $this->location;
2906:             }
2907: 
2908:             if ($this->description) {
2909:                 $tooltip .= "\n\n" . Horde_String::wrap($this->description);
2910:             }
2911:         }
2912: 
2913:         return $tooltip;
2914:     }
2915: 
2916:     /**
2917:      * @return string  The time range of the event ("All Day", "1:00pm-3:00pm",
2918:      *                 "08:00-22:00").
2919:      */
2920:     public function getTimeRange()
2921:     {
2922:         if ($this->isAllDay()) {
2923:             return _("All day");
2924:         } elseif (($cmp = $this->start->compareDate($this->end)) > 0) {
2925:             $df = $GLOBALS['prefs']->getValue('date_format');
2926:             if ($cmp > 0) {
2927:                 return $this->end->strftime($df) . '-'
2928:                     . $this->start->strftime($df);
2929:             } else {
2930:                 return $this->start->strftime($df) . '-'
2931:                     . $this->end->strftime($df);
2932:             }
2933:         } else {
2934:             $twentyFour = $GLOBALS['prefs']->getValue('twentyFour');
2935:             return $this->start->format($twentyFour ? 'G:i' : 'g:ia')
2936:                 . '-'
2937:                 . $this->end->format($twentyFour ? 'G:i' : 'g:ia');
2938:         }
2939:     }
2940: 
2941:     /**
2942:      * @return string  The CSS class for the event based on its status.
2943:      */
2944:     public function getStatusClass()
2945:     {
2946:         switch ($this->status) {
2947:         case Kronolith::STATUS_CANCELLED:
2948:             return 'kronolithEventCancelled';
2949: 
2950:         case Kronolith::STATUS_TENTATIVE:
2951:         case Kronolith::STATUS_FREE:
2952:             return 'kronolithEventTentative';
2953:         }
2954: 
2955:         return 'kronolithEvent';
2956:     }
2957: 
2958:     private function _formIDEncode($id)
2959:     {
2960:         return str_replace(array('[', ']'),
2961:                            array('_', ''),
2962:                            $id);
2963:     }
2964: 
2965: }
2966: 
API documentation generated by ApiGen