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 external API interface.
   4:  *
   5:  * This file defines Kronolith's external API interface. Other applications
   6:  * can interact with Kronolith through this API.
   7:  *
   8:  * @package Kronolith
   9:  */
  10: class Kronolith_Api extends Horde_Registry_Api
  11: {
  12:     /**
  13:      * Links.
  14:      *
  15:      * @var array
  16:      */
  17:     public $links = array(
  18:         'show' => '%application%/event.php?calendar=|calendar|&eventID=|event|&uid=|uid|'
  19:     );
  20: 
  21:     /**
  22:      * Returns the share helper prefix
  23:      *
  24:      * @return string
  25:      */
  26:     public function shareHelp()
  27:     {
  28:         return 'shares';
  29:     }
  30: 
  31:     /**
  32:      * Returns the last modification timestamp for the given uid.
  33:      *
  34:      * @param string $uid      The uid to look for.
  35:      *
  36:      * @return integer  The timestamp for the last modification of $uid.
  37:      */
  38:     public function modified($uid)
  39:     {
  40:         $modified = $this->getActionTimestamp($uid, 'modify');
  41:         if (empty($modified)) {
  42:             $modified = $this->getActionTimestamp($uid, 'add');
  43:         }
  44:         return $modified;
  45:     }
  46: 
  47:     /**
  48:      * Browse through Kronolith's object tree.
  49:      *
  50:      * @param string $path       The level of the tree to browse.
  51:      * @param array $properties  The item properties to return. Defaults to 'name',
  52:      *                           'icon', and 'browseable'.
  53:      *
  54:      * @return array  The contents of $path
  55:      * @throws Kronolith_Exception
  56:      */
  57:     public function browse($path = '', $properties = array())
  58:     {
  59:         global $registry;
  60: 
  61:         // Default properties.
  62:         if (!$properties) {
  63:             $properties = array('name', 'icon', 'browseable');
  64:         }
  65: 
  66:         if (substr($path, 0, 9) == 'kronolith') {
  67:             $path = substr($path, 9);
  68:         }
  69:         $path = trim($path, '/');
  70:         $parts = explode('/', $path);
  71: 
  72:         if (empty($path)) {
  73:             // This request is for a list of all users who have calendars
  74:             // visible to the requesting user.
  75:             $calendars = Kronolith::listInternalCalendars(false, Horde_Perms::READ);
  76:             $owners = array();
  77:             foreach ($calendars as $calendar) {
  78:                 $owners[$calendar->get('owner')] = true;
  79:             }
  80: 
  81:             $results = array();
  82:             foreach (array_keys($owners) as $owner) {
  83:                 $path = 'kronolith/' . $owner;
  84:                 if (in_array('name', $properties)) {
  85:                     $results[$path]['name'] = $owner;
  86:                 }
  87:                 if (in_array('icon', $properties)) {
  88:                     $results[$path]['icon'] = Horde_Themes::img('user.png');
  89:                 }
  90:                 if (in_array('browseable', $properties)) {
  91:                     $results[$path]['browseable'] = true;
  92:                 }
  93:                 if (in_array('contenttype', $properties)) {
  94:                     $results[$path]['contenttype'] =
  95:                         'httpd/unix-directory';
  96:                 }
  97:                 if (in_array('contentlength', $properties)) {
  98:                     $results[$path]['contentlength'] = 0;
  99:                 }
 100:                 if (in_array('modified', $properties)) {
 101:                     $results[$path]['modified'] =
 102:                         $_SERVER['REQUEST_TIME'];
 103:                 }
 104:                 if (in_array('created', $properties)) {
 105:                     $results[$path]['created'] = 0;
 106:                 }
 107: 
 108:                 // CalDAV Properties from RFC 4791 and
 109:                 // draft-desruisseaux-caldav-sched-03
 110:                 $caldavns = 'urn:ietf:params:xml:ns:caldav';
 111:                 $kronolith_rpc_base = $GLOBALS['registry']->get('webroot', 'horde') . '/rpc/kronolith/';
 112:                 if (in_array($caldavns . ':calendar-home-set', $properties)) {
 113:                     $results[$path][$caldavns . ':calendar-home-set'] =  Horde::url($kronolith_rpc_base . urlencode($owner), true);
 114:                 }
 115: 
 116:                 if (in_array($caldavns . ':calendar-user-address-set', $properties)) {
 117:                     // FIXME: Add the calendar owner's email address from
 118:                     // their Horde Identity
 119:                 }
 120:             }
 121:             return $results;
 122: 
 123:         } elseif (count($parts) == 1) {
 124:             // This request is for all calendars owned by the requested user
 125:             $calendars = $GLOBALS['kronolith_shares']->listShares(
 126:                 $GLOBALS['registry']->getAuth(),
 127:                 array('perm' => Horde_Perms::SHOW,
 128:                       'attributes' => $parts[0]));
 129:             $results = array();
 130:             foreach ($calendars as $calendarId => $calendar) {
 131:                 $retpath = 'kronolith/' . $parts[0] . '/' . $calendarId;
 132:                 if (in_array('name', $properties)) {
 133:                     $results[$retpath]['name'] = sprintf(_("Events from %s"), $calendar->get('name'));
 134:                     $results[$retpath . '.ics']['name'] = $calendar->get('name');
 135:                 }
 136:                 if (in_array('displayname', $properties)) {
 137:                     $results[$retpath]['displayname'] = rawurlencode($calendar->get('name'));
 138:                     $results[$retpath . '.ics']['displayname'] = rawurlencode($calendar->get('name')) . '.ics';
 139:                 }
 140:                 if (in_array('icon', $properties)) {
 141:                     $results[$retpath]['icon'] = Horde_Themes::img('kronolith.png');
 142:                     $results[$retpath . '.ics']['icon'] = Horde_Themes::img('mime/icalendar.png');
 143:                 }
 144:                 if (in_array('browseable', $properties)) {
 145:                     $results[$retpath]['browseable'] = $calendar->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::READ);
 146:                     $results[$retpath . '.ics']['browseable'] = false;
 147:                 }
 148:                 if (in_array('contenttype', $properties)) {
 149:                     $results[$retpath]['contenttype'] = 'httpd/unix-directory';
 150:                     $results[$retpath . '.ics']['contenttype'] = 'text/calendar';
 151:                 }
 152:                 if (in_array('contentlength', $properties)) {
 153:                     $results[$retpath]['contentlength'] = 0;
 154:                     // FIXME: This is a hack.  If the content length is longer
 155:                     // than the actual data then some WebDAV clients will
 156:                     // report an error when the file EOF is received.  Ideally
 157:                     // we should determine the actual size of the .ics and
 158:                     // report it here, but the performance hit may be
 159:                     // prohibitive.  This requires further investigation.
 160:                     $results[$retpath . '.ics']['contentlength'] = 1;
 161:                 }
 162:                 if (in_array('modified', $properties)) {
 163:                     $results[$retpath]['modified'] = $_SERVER['REQUEST_TIME'];
 164:                     $results[$retpath . '.ics']['modified'] = $_SERVER['REQUEST_TIME'];
 165:                 }
 166:                 if (in_array('created', $properties)) {
 167:                     $results[$retpath]['created'] = 0;
 168:                     $results[$retpath . '.ics']['created'] = 0;
 169:                 }
 170:             }
 171:             return $results;
 172: 
 173:         } elseif (count($parts) == 2 &&
 174:                   array_key_exists($parts[1], Kronolith::listInternalCalendars(false, Horde_Perms::READ))) {
 175:             // This request is browsing into a specific calendar.  Generate
 176:             // the list of items and represent them as files within the
 177:             // directory.
 178:             $kronolith_driver = Kronolith::getDriver(null, $parts[1]);
 179:             $events = $kronolith_driver->listEvents();
 180:             $icon = Horde_Themes::img('mime/icalendar.png');
 181:             $results = array();
 182:             foreach ($events as $dayevents) {
 183:                 foreach ($dayevents as $event) {
 184:                     $key = 'kronolith/' . $path . '/' . $event->id;
 185:                     if (in_array('name', $properties)) {
 186:                         $results[$key]['name'] = $event->getTitle();
 187:                     }
 188:                     if (in_array('icon', $properties)) {
 189:                         $results[$key]['icon'] = $icon;
 190:                     }
 191:                     if (in_array('browseable', $properties)) {
 192:                         $results[$key]['browseable'] = false;
 193:                     }
 194:                     if (in_array('contenttype', $properties)) {
 195:                         $results[$key]['contenttype'] = 'text/calendar';
 196:                     }
 197:                     if (in_array('contentlength', $properties)) {
 198:                         // FIXME: This is a hack.  If the content length is
 199:                         // longer than the actual data then some WebDAV
 200:                         // clients will report an error when the file EOF is
 201:                         // received.  Ideally we should determine the actual
 202:                         // size of the data and report it here, but the
 203:                         // performance hit may be prohibitive.  This requires
 204:                         // further investigation.
 205:                         $results[$key]['contentlength'] = 1;
 206:                     }
 207:                     if (in_array('modified', $properties)) {
 208:                         $results[$key]['modified'] = $this->modified($event->uid);
 209:                     }
 210:                     if (in_array('created', $properties)) {
 211:                         $results[$key]['created'] = $this->getActionTimestamp($event->uid, 'add');
 212:                     }
 213:                 }
 214:             }
 215:             return $results;
 216:         } else {
 217:             // The only valid request left is for either a specific event or
 218:             // for the entire calendar.
 219:             if (count($parts) == 3 &&
 220:                 array_key_exists($parts[1], Kronolith::listInternalCalendars(false, Horde_Perms::READ))) {
 221:                 // This request is for a specific item within a given calendar.
 222:                 $event = Kronolith::getDriver(null, $parts[1])->getEvent($parts[2]);
 223: 
 224:                 $result = array(
 225:                     'data' => $this->export($event->uid, 'text/calendar'),
 226:                     'mimetype' => 'text/calendar');
 227:                 $modified = $this->modified($event->uid);
 228:                 if (!empty($modified)) {
 229:                     $result['mtime'] = $modified;
 230:                 }
 231:                 return $result;
 232:             } elseif (count($parts) == 2 &&
 233:                       substr($parts[1], -4, 4) == '.ics' &&
 234:                       array_key_exists(substr($parts[1], 0, -4), Kronolith::listInternalCalendars(false, Horde_Perms::READ))) {
 235:                 // This request is for an entire calendar (calendar.ics).
 236:                 $ical_data = $this->exportCalendar(substr($parts[1], 0, -4), 'text/calendar');
 237:                 $result = array('data'          => $ical_data,
 238:                                 'mimetype'      => 'text/calendar',
 239:                                 'contentlength' => strlen($ical_data),
 240:                                 'mtime'         => $_SERVER['REQUEST_TIME']);
 241: 
 242:                 return $result;
 243:             } else {
 244:                 // All other requests are a 404: Not Found
 245:                 return false;
 246:             }
 247:         }
 248:     }
 249: 
 250:     /**
 251:      * Saves a file into the Kronolith tree.
 252:      *
 253:      * @param string $path          The path where to PUT the file.
 254:      * @param string $content       The file content.
 255:      * @param string $content_type  The file's content type.
 256:      *
 257:      * @return array  The event UIDs.
 258:      * @throws Kronolith_Exception
 259:      */
 260:     public function put($path, $content, $content_type)
 261:     {
 262:         if (substr($path, 0, 9) == 'kronolith') {
 263:             $path = substr($path, 9);
 264:         }
 265:         $path = trim($path, '/');
 266:         $parts = explode('/', $path);
 267: 
 268:         if (count($parts) == 2 && substr($parts[1], -4) == '.ics') {
 269:             // Workaround for WebDAV clients that are not smart enough to send
 270:             // the right content type.  Assume text/calendar.
 271:             if ($content_type == 'application/octet-stream') {
 272:                 $content_type = 'text/calendar';
 273:             }
 274:             $calendar = substr($parts[1], 0, -4);
 275:         } elseif (count($parts) == 3) {
 276:             $calendar = $parts[1];
 277:             // Workaround for WebDAV clients that are not smart enough to send
 278:             // the right content type.  Assume text/calendar.
 279:             if ($content_type == 'application/octet-stream') {
 280:                 $content_type = 'text/calendar';
 281:             }
 282:         } else {
 283:             throw new Kronolith_Exception("Invalid calendar data supplied.");
 284:         }
 285: 
 286:         if (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) {
 287:             // FIXME: Should we attempt to create a calendar based on the
 288:             // filename in the case that the requested calendar does not
 289:             // exist?
 290:             throw new Kronolith_Exception("Calendar does not exist or no permission to edit");
 291:         }
 292: 
 293:         // Store all currently existings UIDs. Use this info to delete UIDs not
 294:         // present in $content after processing.
 295:         $ids = array();
 296:         $uids_remove = array_flip($this->listUids($calendar));
 297: 
 298:         switch ($content_type) {
 299:         case 'text/calendar':
 300:         case 'text/x-vcalendar':
 301:             $iCal = new Horde_Icalendar();
 302:             if (!($content instanceof Horde_Icalendar_Vevent)) {
 303:                 if (!$iCal->parsevCalendar($content)) {
 304:                     throw new Kronolith_Exception(_("There was an error importing the iCalendar data."));
 305:                 }
 306:             } else {
 307:                 $iCal->addComponent($content);
 308:             }
 309: 
 310:             $kronolith_driver = Kronolith::getDriver();
 311:             foreach ($iCal->getComponents() as $content) {
 312:                 if ($content instanceof Horde_Icalendar_Vevent) {
 313:                     $event = $kronolith_driver->getEvent();
 314:                     $event->fromiCalendar($content);
 315:                     $uid = $event->uid;
 316:                     // Remove from uids_remove list so we won't delete in the
 317:                     // end.
 318:                     if (isset($uids_remove[$uid])) {
 319:                         unset($uids_remove[$uid]);
 320:                     }
 321:                     try {
 322:                         $existing_event = $kronolith_driver->getByUID($uid, array($calendar));
 323:                         // Check if our event is newer then the existing - get
 324:                         // the event's history.
 325:                         $created = $modified = null;
 326:                         try {
 327:                             $log = $GLOBALS['injector']->getInstance('Horde_History')->getHistory('kronolith:' . $calendar . ':' . $uid);
 328:                             foreach ($log as $entry) {
 329:                                 switch ($entry['action']) {
 330:                                 case 'add':
 331:                                     $created = $entry['ts'];
 332:                                     break;
 333: 
 334:                                 case 'modify':
 335:                                     $modified = $entry['ts'];
 336:                                     break;
 337:                                 }
 338:                             }
 339:                         } catch (Horde_Exception $e) {
 340:                         }
 341:                         if (empty($modified) && !empty($created)) {
 342:                             $modified = $created;
 343:                         }
 344:                         try {
 345:                             if (!empty($modified) &&
 346:                                 $modified >= $content->getAttribute('LAST-MODIFIED')) {
 347:                                 // LAST-MODIFIED timestamp of existing entry
 348:                                 // is newer: don't replace it.
 349:                                 continue;
 350:                             }
 351:                         } catch (Horde_Icalendar_Exception $e) {
 352:                         }
 353: 
 354:                         // Don't change creator/owner.
 355:                         $event->creator = $existing_event->creator;
 356:                     } catch (Horde_Exception_NotFound $e) {
 357:                     }
 358: 
 359:                     // Save entry.
 360:                     $saved = $event->save();
 361:                     $ids[] = $event->uid;
 362:                 }
 363:             }
 364:             break;
 365: 
 366:         default:
 367:             throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $content_type));
 368:         }
 369: 
 370:         if (Kronolith::hasPermission($calendar, Horde_Perms::DELETE)) {
 371:             foreach (array_keys($uids_remove) as $uid) {
 372:                 $this->delete($uid);
 373:             }
 374:         }
 375: 
 376:         return $ids;
 377:     }
 378: 
 379:     /**
 380:      * Deletes a file from the Kronolith tree.
 381:      *
 382:      * @param string $path  The path to the file.
 383:      *
 384:      * @throws Kronolith_Exception
 385:      */
 386:     public function path_delete($path)
 387:     {
 388:         if (substr($path, 0, 9) == 'kronolith') {
 389:             $path = substr($path, 9);
 390:         }
 391:         $path = trim($path, '/');
 392:         $parts = explode('/', $path);
 393: 
 394:         if (substr($parts[1], -4) == '.ics') {
 395:             $calendarId = substr($parts[1], 0, -4);
 396:         } else {
 397:             $calendarId = $parts[1];
 398:         }
 399: 
 400:         if (!(count($parts) == 2 || count($parts) == 3) ||
 401:             !Kronolith::hasPermission($calendarId, Horde_Perms::DELETE)) {
 402:                 throw new Kronolith_Exception("Calendar does not exist or no permission to delete");
 403:             }
 404: 
 405:         if (count($parts) == 3) {
 406:             // Delete just a single entry
 407:             return Kronolith::getDriver(null, $calendarId)->deleteEvent($parts[2]);
 408:         } else {
 409:             // Delete the entire calendar
 410:             try {
 411:                 Kronolith::getDriver()->delete($calendarId);
 412:                 // Remove share and all groups/permissions.
 413:                 $share = $GLOBALS['kronolith_shares']->getShare($calendarId);
 414:                 $result = $GLOBALS['kronolith_shares']->removeShare($share);
 415:             } catch (Exception $e) {
 416:                 throw new Kronolith_Exception(sprintf(_("Unable to delete calendar \"%s\": %s"), $calendarId, $e->getMessage()));
 417:             }
 418:         }
 419:     }
 420: 
 421:     /**
 422:      * Returns all calendars a user has access to, according to several
 423:      * parameters/permission levels.
 424:      *
 425:      * @param boolean $owneronly   Only return calenders that this user owns?
 426:      *                             Defaults to false.
 427:      * @param integer $permission  The permission to filter calendars by.
 428:      *
 429:      * @return array  The calendar list.
 430:      */
 431:     public function listCalendars($owneronly = false, $permission = null)
 432:     {
 433:         if (is_null($permission)) {
 434:             $permission = Horde_Perms::SHOW;
 435:         }
 436:         return array_keys(Kronolith::listInternalCalendars($owneronly, $permission));
 437:     }
 438: 
 439:     /**
 440:      * Returns the ids of all the events that happen within a time period.
 441:      * Only includes recurring events once per time period, and does not include
 442:      * events that represent exceptions, making this method useful for syncing
 443:      * purposes. For more control, use the listEvents method.
 444:      *
 445:      * @param string $calendars      The calendar to check for events.
 446:      * @param object $startstamp    The start of the time range.
 447:      * @param object $endstamp      The end of the time range.
 448:      *
 449:      * @return array  The event ids happening in this time period.
 450:      * @throws Kronolith_Exception
 451:      */
 452:     public function listUids($calendars = null, $startstamp = 0, $endstamp = 0)
 453:     {
 454:         if (empty($calendars)) {
 455:             $calendars = Kronolith::getSyncCalendars();
 456:         } elseif (!is_array($calendars)) {
 457:             $calendars = array($calendars);
 458:         }
 459: 
 460:         $driver = Kronolith::getDriver();
 461:         $results = array();
 462:         foreach ($calendars as $calendar) {
 463:             if (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) {
 464:                 Horde::logMessage(sprintf(
 465:                     _("Permission Denied or Calendar Not Found: %s - skipping."),
 466:                     $calendar));
 467:                 continue;
 468:             }
 469:             try {
 470:                 $driver->open($calendar);
 471:                 $events = $driver->listEvents(
 472:                     $startstamp ? new Horde_Date($startstamp) : null,
 473:                     $endstamp   ? new Horde_Date($endstamp)   : null,
 474:                     false,  // recurrence
 475:                     false,  // alarm
 476:                     false,  // no json cache
 477:                     false,  // Don't cover dates
 478:                     true,   // Hide exceptions
 479:                     false); // No tags
 480:                 Kronolith::mergeEvents($results, $events);
 481:             } catch (Kronolith_Exception $e) {
 482:                 Horde::logMessage($e);
 483:             }
 484:         }
 485:         $uids = array();
 486:         foreach ($results as $dayevents) {
 487:             foreach ($dayevents as $event) {
 488:                 $uids[] = $event->uid;
 489:             }
 490:         }
 491: 
 492:         return $uids;
 493:     }
 494: 
 495:     /**
 496:      * Returns an array of UIDs for events that have had $action happen since
 497:      * $timestamp.
 498:      *
 499:      * @param string  $action     The action to check for - add, modify, or delete.
 500:      * @param integer $timestamp  The time to start the search.
 501:      * @param string  $calendar   The calendar to search in.
 502:      * @param integer $end        The optional ending timestamp
 503:      *
 504:      * @return array  An array of UIDs matching the action and time criteria.
 505:      *
 506:      * @throws Kronolith_Exception
 507:      * @throws Horde_History_Exception
 508:      * @throws InvalidArgumentException
 509:      */
 510:     public function listBy($action, $timestamp, $calendar = null, $end = null)
 511:     {
 512:         if (empty($calendar)) {
 513:             $cs = Kronolith::getSyncCalendars($action == 'delete');
 514:             $results = array();
 515:             foreach ($cs as $c) {
 516:                 $results = array_merge(
 517:                     $results, $this->listBy($action, $timestamp, $c, $end));
 518:             }
 519:             return $results;
 520:         }
 521:         $filter = array(array('op' => '=', 'field' => 'action', 'value' => $action));
 522:         if (!empty($end)) {
 523:             $filter[] = array('op' => '<', 'field' => 'ts', 'value' => $end);
 524:         }
 525:         $histories = $GLOBALS['injector']
 526:             ->getInstance('Horde_History')
 527:             ->getByTimestamp('>', $timestamp, $filter, 'kronolith:' . $calendar);
 528: 
 529:         // Strip leading kronolith:username:.
 530:         return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
 531:     }
 532: 
 533:     /**
 534:      * Method for obtaining all server changes between two timestamps. Basically
 535:      * a wrapper around listBy(), but returns an array containing all adds,
 536:      * edits and deletions. If $ignoreExceptions is true, events representing
 537:      * recurring event exceptions will not be included in the results.
 538:      *
 539:      * @param integer $start             The starting timestamp
 540:      * @param integer $end               The ending timestamp.
 541:      * @param boolean $ignoreExceptions  Do not include exceptions in results.
 542:      *
 543:      * @return array  An hash with 'add', 'modify' and 'delete' arrays.
 544:      * @throws Horde_Exception_PermissionDenied
 545:      * @throws Kronolith_Exception
 546:      */
 547:     public function getChanges($start, $end, $ignoreExceptions = true)
 548:     {
 549:         // Only get the calendar once
 550:         $cs = Kronolith::getSyncCalendars();
 551:         $changes = array(
 552:             'add' => array(),
 553:             'modify' => array(),
 554:             'delete' => array());
 555: 
 556:         foreach ($cs as $c) {
 557:              // New events
 558:             $uids = $this->listBy('add', $start, $c, $end);
 559:             if ($ignoreExceptions) {
 560:                 foreach ($uids as $uid) {
 561:                     try {
 562:                         $event = Kronolith::getDriver()->getByUID($uid);
 563:                     } catch (Exception $e) {
 564:                         continue;
 565:                     }
 566:                     if (empty($event->baseid)) {
 567:                         $changes['add'][] = $uid;
 568:                     }
 569:                 }
 570:             } else {
 571:                 $changes['add'] = array_keys(array_flip(array_merge($changes['add'], $uids)));
 572:             }
 573: 
 574:             // Edits
 575:             $uids = $this->listBy('modify', $start, $c, $end);
 576:             if ($ignoreExceptions) {
 577:                 foreach ($uids as $uid) {
 578:                     try {
 579:                         $event = Kronolith::getDriver()->getByUID($uid);
 580:                     } catch (Exception $e) {
 581:                         continue;
 582:                     }
 583:                     if (empty($event->baseid)) {
 584:                         $changes['modify'][] = $uid;
 585:                     }
 586:                 }
 587:             } else {
 588:                 $changes['modify'] = array_keys(array_flip(array_merge($changes['modify'], $uids)));
 589:             }
 590:             // No way to figure out if this was an exception, so we must include all
 591:             $changes['delete'] = array_keys(
 592:                 array_flip(array_merge($changes['delete'], $this->listBy('delete', $start, $c, $end))));
 593:         }
 594: 
 595:         return $changes;
 596:     }
 597: 
 598: 
 599:     /**
 600:      * Returns the timestamp of an operation for a given uid an action
 601:      *
 602:      * @param string $uid      The uid to look for.
 603:      * @param string $action   The action to check for - add, modify, or delete.
 604:      * @param string $calendar The calendar to search in.
 605:      *
 606:      * @return integer  The timestamp for this action.
 607:      *
 608:      * @throws Kronolith_Exception
 609:      * @throws InvalidArgumentException
 610:      */
 611:     public function getActionTimestamp($uid, $action, $calendar = null)
 612:     {
 613:         if (empty($calendar)) {
 614:             $calendar = Kronolith::getDefaultCalendar();
 615:         } elseif (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) {
 616:             throw new Horde_Exception_PermissionDenied();
 617:         }
 618: 
 619:         return $GLOBALS['injector']->getInstance('Horde_History')->getActionTimestamp('kronolith:' . $calendar . ':' . $uid, $action);
 620:     }
 621: 
 622:     /**
 623:      * Imports an event represented in the specified content type.
 624:      *
 625:      * @param string $content      The content of the event.
 626:      * @param string $contentType  What format is the data in? Currently supports:
 627:      *                             <pre>
 628:      *                             text/calendar
 629:      *                             text/x-vcalendar
 630:      *                             </pre>
 631:      * @param string $calendar     What calendar should the event be added to?
 632:      *
 633:      * @return array  The event's UID.
 634:      * @throws Kronolith_Exception
 635:      */
 636:     public function import($content, $contentType, $calendar = null)
 637:     {
 638:         if (!isset($calendar)) {
 639:             $calendar = Kronolith::getDefaultCalendar(Horde_Perms::EDIT);
 640:         } elseif (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) {
 641:             throw new Horde_Exception_PermissionDenied();
 642:         }
 643: 
 644:         $kronolith_driver = Kronolith::getDriver(null, $calendar);
 645: 
 646:         switch ($contentType) {
 647:         case 'text/calendar':
 648:         case 'text/x-vcalendar':
 649:             $iCal = new Horde_Icalendar();
 650:             if (!($content instanceof Horde_Icalendar_Vevent)) {
 651:                 if (!$iCal->parsevCalendar($content)) {
 652:                     throw new Kronolith_Exception(_("There was an error importing the iCalendar data."));
 653:                 }
 654:             } else {
 655:                 $iCal->addComponent($content);
 656:             }
 657: 
 658:             $components = $iCal->getComponents();
 659:             if (count($components) == 0) {
 660:                 throw new Kronolith_Exception(_("No iCalendar data was found."));
 661:             }
 662: 
 663:             $ids = array();
 664:             $recurrences = array();
 665:             foreach ($components as $content) {
 666:                 if ($content instanceof Horde_Icalendar_Vevent) {
 667:                     // Need to ensure that the original recurring event is
 668:                     // added before any of the instance exceptions. Easiest way
 669:                     // to do that is just add all the recurrence-id entries last
 670:                     try {
 671:                         $recurrenceId = $content->getAttribute('RECURRENCE-ID');
 672:                         $recurrences[] = $content;
 673:                     } catch (Horde_Icalendar_Exception $e) {
 674:                         $ids[] = $this->_addiCalEvent($content, $kronolith_driver);
 675:                     }
 676:                 }
 677:             }
 678: 
 679:             if (count($ids) == 0) {
 680:                 throw new Kronolith_Exception(_("No iCalendar data was found."));
 681:             } else if (count($ids) == 1) {
 682:                 return $ids[0];
 683:             }
 684: 
 685:             // Now add all the exception instances
 686:             foreach ($recurrences as $recurrence) {
 687:                 $ids[] = $this->_addiCalEvent($recurrence, $kronolith_driver);
 688:             }
 689: 
 690:             return $ids;
 691: 
 692:             case 'activesync':
 693:                 $event = $kronolith_driver->getEvent();
 694:                 $event->fromASAppointment($content);
 695:                 $event->save();
 696:                 return $event->uid;
 697:         }
 698: 
 699:         throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
 700:     }
 701: 
 702:     /**
 703:      * Imports a single vEvent part to storage.
 704:      *
 705:      * @param Horde_Icalendar_Vevent $content  The vEvent part
 706:      * @param Kronolith_Driver $driver         The kronolith driver
 707:      *
 708:      * @return string  The new event's uid
 709:      */
 710:     protected function _addiCalEvent($content, $driver)
 711:     {
 712:         $event = $driver->getEvent();
 713:         $event->fromiCalendar($content);
 714:         // Check if the entry already exists in the data source,
 715:         // first by UID.
 716:         $uid = $event->uid;
 717:         try {
 718:             $existing_event = $driver->getByUID($uid, array($driver->calendar));
 719:             throw new Kronolith_Exception(sprintf(_("%s Already Exists"), $uid));
 720:         } catch (Horde_Exception $e) {}
 721:         $result = $driver->search($event);
 722:         // Check if the match really is an exact match:
 723:         if (is_array($result) && count($result) > 0) {
 724:             foreach($result as $match) {
 725:                 if ($match->start == $event->start &&
 726:                     $match->end == $event->end &&
 727:                     $match->title == $event->title &&
 728:                     $match->location == $event->location &&
 729:                     $match->hasPermission(Horde_Perms::EDIT)) {
 730:                         throw new Kronolith_Exception(sprintf(_("%s Already Exists"), $match->uid));
 731:                     }
 732:             }
 733:         }
 734:         $event->save();
 735: 
 736:         return $event->uid;
 737:     }
 738: 
 739:     /**
 740:      * Imports an event parsed from a string.
 741:      *
 742:      * @param string $text      The text to parse into an event
 743:      * @param string $calendar  The calendar into which the event will be
 744:      *                          imported.  If 'null', the user's default
 745:      *                          calendar will be used.
 746:      *
 747:      * @return array  The UID of all events that were added.
 748:      * @throws Kronolith_Exception
 749:      */
 750:     public function quickAdd($text, $calendar = null)
 751:     {
 752:         if (!isset($calendar)) {
 753:             $calendar = Kronolith::getDefaultCalendar(Horde_Perms::EDIT);
 754:         } elseif (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) {
 755:             throw new Horde_Exception_PermissionDenied();
 756:         }
 757:         $event = Kronolith::quickAdd($text, $calendar);
 758:         return $event->uid;
 759:     }
 760: 
 761:     /**
 762:      * Exports an event, identified by UID, in the requested content type.
 763:      *
 764:      * @param string $uid         Identify the event to export.
 765:      * @param string $contentType  What format should the data be in?
 766:      *                            A string with one of:
 767:      *                            <pre>
 768:      *                             text/calendar (VCALENDAR 2.0. Recommended as
 769:      *                                            this is specified in rfc2445)
 770:      *                             text/x-vcalendar (old VCALENDAR 1.0 format.
 771:      *                                              Still in wide use)
 772:      *                            </pre>
 773:      *
 774:      * @return string  The requested data.
 775:      * @throws Kronolith_Exception
 776:      * @throws Horde_Exception_NotFound
 777:      */
 778:     public function export($uid, $contentType)
 779:     {
 780:         global $kronolith_shares;
 781: 
 782:         $event = Kronolith::getDriver()->getByUID($uid);
 783:         if (!$event->hasPermission(Horde_Perms::READ)) {
 784:             throw new Horde_Exception_PermissionDenied();
 785:         }
 786: 
 787:         $version = '2.0';
 788:         switch ($contentType) {
 789:         case 'text/x-vcalendar':
 790:             $version = '1.0';
 791:         case 'text/calendar':
 792:             $share = $kronolith_shares->getShare($event->calendar);
 793: 
 794:             $iCal = new Horde_Icalendar($version);
 795:             $iCal->setAttribute('X-WR-CALNAME', $share->get('name'));
 796: 
 797:             // Create a new vEvent.
 798:             $iCal->addComponent($event->toiCalendar($iCal));
 799: 
 800:             return $iCal->exportvCalendar();
 801: 
 802:         case 'activesync':
 803:             return $event->toASAppointment();
 804:         }
 805: 
 806:         throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
 807:     }
 808: 
 809:     /**
 810:      * Exports a calendar in the requested content type.
 811:      *
 812:      * @param string $calendar    The calendar to export.
 813:      * @param string $contentType  What format should the data be in?
 814:      *                             A string with one of:
 815:      *                             <pre>
 816:      *                             text/calendar (VCALENDAR 2.0. Recommended as
 817:      *                                            this is specified in rfc2445)
 818:      *                             text/x-vcalendar (old VCALENDAR 1.0 format.
 819:      *                                              Still in wide use)
 820:      *                             </pre>
 821:      *
 822:      * @return string  The iCalendar representation of the calendar.
 823:      * @throws Kronolith_Exception
 824:      */
 825:     public function exportCalendar($calendar, $contentType)
 826:     {
 827:         global $kronolith_shares;
 828: 
 829:         if (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) {
 830:             throw new Horde_Exception_PermissionDenied();
 831:         }
 832: 
 833:         $kronolith_driver = Kronolith::getDriver(null, $calendar);
 834:         $events = $kronolith_driver->listEvents(null, null, false, false, false, false, false, true);
 835: 
 836:         $version = '2.0';
 837:         switch ($contentType) {
 838:         case 'text/x-vcalendar':
 839:             $version = '1.0';
 840:         case 'text/calendar':
 841:             $share = $kronolith_shares->getShare($calendar);
 842: 
 843:             $iCal = new Horde_Icalendar($version);
 844:             $iCal->setAttribute('X-WR-CALNAME', $share->get('name'));
 845:             if (strlen($share->get('desc'))) {
 846:                 $iCal->setAttribute('X-WR-CALDESC', $share->get('desc'));
 847:             }
 848: 
 849:             foreach ($events as $dayevents) {
 850:                 foreach ($dayevents as $event) {
 851:                     $iCal->addComponent($event->toiCalendar($iCal));
 852:                 }
 853:             }
 854: 
 855:             return $iCal->exportvCalendar();
 856:         }
 857: 
 858:         throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
 859:     }
 860: 
 861:     /**
 862:      * Deletes an event identified by UID.
 863:      *
 864:      * @param string|array $uid     A single UID or an array identifying the
 865:      *                              event(s) to delete.
 866:      *
 867:      * @param string $recurrenceId  The reccurenceId for the event instance, if
 868:      *                              this is a deletion of a recurring event
 869:      *                              instance ($uid must not be an array).
 870:      *
 871:      * @throws Kronolith_Exception
 872:      */
 873:     public function delete($uid, $recurrenceId = null)
 874:     {
 875:         // Handle an array of UIDs for convenience of deleting multiple events
 876:         // at once.
 877:         if (is_array($uid)) {
 878:             foreach ($uid as $g) {
 879:                 $result = $this->delete($g);
 880:             }
 881:             return;
 882:         }
 883: 
 884:         $kronolith_driver = Kronolith::getDriver();
 885:         $events = $kronolith_driver->getByUID($uid, null, true);
 886: 
 887:         $event = null;
 888:         if ($GLOBALS['registry']->isAdmin()) {
 889:             $event = $events[0];
 890:         }
 891: 
 892:         // First try the user's own calendars.
 893:         if (empty($event)) {
 894:             $ownerCalendars = Kronolith::listInternalCalendars(true, Horde_Perms::DELETE);
 895:             foreach ($events as $ev) {
 896:                 if ($GLOBALS['registry']->isAdmin() || isset($ownerCalendars[$ev->calendar])) {
 897:                     $kronolith_driver->open($ev->calendar);
 898:                     $event = $ev;
 899:                     break;
 900:                 }
 901:             }
 902:         }
 903: 
 904:         // If not successful, try all calendars the user has access to.
 905:         if (empty($event)) {
 906:             $deletableCalendars = Kronolith::listInternalCalendars(false, Horde_Perms::DELETE);
 907:             foreach ($events as $ev) {
 908:                 if (isset($deletableCalendars[$ev->calendar])) {
 909:                     $kronolith_driver->open($ev->calendar);
 910:                     $event = $ev;
 911:                     break;
 912:                 }
 913:             }
 914:         }
 915: 
 916:         if (empty($event)) {
 917:             throw new Horde_Exception_PermissionDenied();
 918:         }
 919: 
 920:         if ($recurrenceId && $event->recurs()) {
 921:             $deleteDate = new Horde_Date($recurrenceId);
 922:             $event->recurrence->addException($deleteDate->format('Y'), $deleteDate->format('m'), $deleteDate->format('d'));
 923:             $event->save();
 924:         } elseif ($recurrenceId) {
 925:             throw new Kronolith_Exception(_("Unable to delete event. An exception date was provided but the event does not seem to be recurring."));
 926:         } else {
 927:             $kronolith_driver->deleteEvent($event->id);
 928:         }
 929:     }
 930: 
 931:     /**
 932:      * Replaces the event identified by UID with the content represented in the
 933:      * specified contentType.
 934:      *
 935:      * @param string $uid          Idenfity the event to replace.
 936:      * @param mixed  $content      The content of the event. String or
 937:      *                             Horde_Icalendar_Vevent
 938:      * @param string $contentType  What format is the data in? Currently supports:
 939:      *                             text/calendar
 940:      *                             text/x-vcalendar
 941:      *                             (Ignored if content is Horde_Icalendar_Vevent)
 942:      *
 943:      * @throws Kronolith_Exception
 944:      */
 945:     public function replace($uid, $content, $contentType)
 946:     {
 947:         $event = Kronolith::getDriver()->getByUID($uid);
 948: 
 949:         if (!$event->hasPermission(Horde_Perms::EDIT) ||
 950:             ($event->private && $event->creator != $GLOBALS['registry']->getAuth())) {
 951:             throw new Horde_Exception_PermissionDenied();
 952:         }
 953: 
 954:         if ($content instanceof Horde_Icalendar_Vevent) {
 955:             $component = $content;
 956:         } elseif ($content instanceof Horde_ActiveSync_Message_Appointment) {
 957:             $event->fromASAppointment($content);
 958:             $event->save();
 959:             $event->uid = $uid;
 960:             return;
 961:         } else {
 962:             switch ($contentType) {
 963:             case 'text/calendar':
 964:             case 'text/x-vcalendar':
 965:                 if (!($content instanceof Horde_Icalendar_Vevent)) {
 966:                     $iCal = new Horde_Icalendar();
 967:                     if (!$iCal->parsevCalendar($content)) {
 968:                         throw new Kronolith_Exception(_("There was an error importing the iCalendar data."));
 969:                     }
 970: 
 971:                     $components = $iCal->getComponents();
 972:                     $component = null;
 973:                     foreach ($components as $content) {
 974:                         if ($content instanceof Horde_Icalendar_Vevent) {
 975:                             if ($component !== null) {
 976:                                 throw new Kronolith_Exception(_("Multiple iCalendar components found; only one vEvent is supported."));
 977:                             }
 978:                             $component = $content;
 979:                         }
 980: 
 981:                     }
 982:                     if ($component === null) {
 983:                         throw new Kronolith_Exception(_("No iCalendar data was found."));
 984:                     }
 985:                 }
 986:                 break;
 987: 
 988:             default:
 989:                 throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
 990:             }
 991:         }
 992: 
 993:         $event->fromiCalendar($component);
 994:         // Ensure we keep the original UID, even when content does not
 995:         // contain one and fromiCalendar creates a new one.
 996:         $event->uid = $uid;
 997:         $event->save();
 998:     }
 999: 
1000:     /**
1001:      * Generates free/busy information for a given time period.
1002:      *
1003:      * @param integer $startstamp  The start of the time period to retrieve.
1004:      * @param integer $endstamp    The end of the time period to retrieve.
1005:      * @param string $calendar     The calendar to view free/busy slots for.
1006:      *                             Defaults to the user's default calendar.
1007:      *
1008:      * @return Horde_Icalendar_Vfreebusy  A freebusy object that covers the
1009:      *                                    specified time period.
1010:      * @throws Kronolith_Exception
1011:      */
1012:     public function getFreeBusy($startstamp = null, $endstamp = null,
1013:                                 $calendar = null)
1014:     {
1015:         if (is_null($calendar)) {
1016:             $calendar = Kronolith::getDefaultCalendar();
1017:         }
1018:         // Free/Busy information is globally available; no permission
1019:         // check is needed.
1020:         return Kronolith_FreeBusy::generate($calendar, $startstamp, $endstamp, true);
1021:     }
1022: 
1023:     /**
1024:      * Retrieves a Kronolith_Event object, given an event UID.
1025:      *
1026:      * @param string $uid  The event's UID.
1027:      *
1028:      * @return Kronolith_Event  A valid Kronolith_Event.
1029:      * @throws Kronolith_Exception
1030:      */
1031:     public function eventFromUID($uid)
1032:     {
1033:         $event = Kronolith::getDriver()->getByUID($uid);
1034:         if (!$event->hasPermission(Horde_Perms::SHOW)) {
1035:             throw new Horde_Exception_PermissionDenied();
1036:         }
1037: 
1038:         return $event;
1039:     }
1040: 
1041:     /**
1042:      * Updates an attendee's response status for a specified event.
1043:      *
1044:      * @param Horde_Icalender_Vevent $response  A Horde_Icalender_Vevent
1045:      *                                          object, with a valid UID
1046:      *                                          attribute that points to an
1047:      *                                          existing event.  This is
1048:      *                                          typically the vEvent portion
1049:      *                                          of an iTip meeting-request
1050:      *                                          response, with the attendee's
1051:      *                                          response in an ATTENDEE
1052:      *                                          parameter.
1053:      * @param string $sender                    The email address of the
1054:      *                                          person initiating the
1055:      *                                          update. Attendees are only
1056:      *                                          updated if this address
1057:      *                                          matches.
1058:      *
1059:      * @throws Kronolith_Exception
1060:      */
1061:     public function updateAttendee($response, $sender = null)
1062:     {
1063:         try {
1064:             $uid = $response->getAttribute('UID');
1065:         } catch (Horde_Icalendar_Exception $e) {
1066:             throw new Kronolith_Exception($e);
1067:         }
1068: 
1069:         $events = Kronolith::getDriver()->getByUID($uid, null, true);
1070: 
1071:         /* First try the user's own calendars. */
1072:         $ownerCalendars = Kronolith::listInternalCalendars(true, Horde_Perms::EDIT);
1073:         $event = null;
1074:         foreach ($events as $ev) {
1075:             if (isset($ownerCalendars[$ev->calendar])) {
1076:                 $event = $ev;
1077:                 break;
1078:             }
1079:         }
1080: 
1081:         /* If not successful, try all calendars the user has access to. */
1082:         if (empty($event)) {
1083:             $editableCalendars = Kronolith::listInternalCalendars(false, Horde_Perms::EDIT);
1084:             foreach ($events as $ev) {
1085:                 if (isset($editableCalendars[$ev->calendar])) {
1086:                     $event = $ev;
1087:                     break;
1088:                 }
1089:             }
1090:         }
1091: 
1092:         if (empty($event) ||
1093:             ($event->private && $event->creator != $GLOBALS['registry']->getAuth())) {
1094:             throw new Horde_Exception_PermissionDenied();
1095:         }
1096: 
1097:         $atnames = $response->getAttribute('ATTENDEE');
1098:         if (!is_array($atnames)) {
1099:             $atnames = array($atnames);
1100:         }
1101:         $atparms = $response->getAttribute('ATTENDEE', true);
1102: 
1103:         $found = false;
1104:         $error = _("No attendees have been updated because none of the provided email addresses have been found in the event's attendees list.");
1105:         $sender_lcase = Horde_String::lower($sender);
1106:         foreach ($atnames as $index => $attendee) {
1107:             if ($response->getAttribute('VERSION') < 2) {
1108:                 $addresses = Horde_Mime_Address::parseAddressList($attendee);
1109:                 if (!count($addresses)) {
1110:                     continue;
1111:                 }
1112:                 $attendee = $addresses[0]['mailbox'];
1113:                 if (isset($addresses[0]['host'])) {
1114:                     $attendee .= '@' . $addresses[0]['host'];
1115:                 }
1116:                 $attendee = Horde_String::lower($attendee);
1117:                 $name = isset($addresses[0]['personal']) ? $addresses[0]['personal'] : null;
1118:             } else {
1119:                 $attendee = str_replace('mailto:', '', Horde_String::lower($attendee));
1120:                 $name = isset($atparms[$index]['CN']) ? $atparms[$index]['CN'] : null;
1121:             }
1122:             if ($event->hasAttendee($attendee)) {
1123:                 if (is_null($sender) || $sender_lcase == $attendee) {
1124:                     $event->addAttendee($attendee, Kronolith::PART_IGNORE, Kronolith::responseFromICal($atparms[$index]['PARTSTAT']), $name);
1125:                     $found = true;
1126:                 } else {
1127:                     $error = _("The attendee hasn't been updated because the update was not sent from the attendee.");
1128:                 }
1129:             }
1130:         }
1131:         $event->save();
1132: 
1133:         if (!$found) {
1134:             throw new Kronolith_Exception($error);
1135:         }
1136:     }
1137: 
1138:     /**
1139:      * Lists events for a given time period.
1140:      *
1141:      * @param integer $startstamp      The start of the time period to
1142:      *                                 retrieve.
1143:      * @param integer $endstamp        The end of the time period to retrieve.
1144:      * @param array   $calendars       The calendars to view events from.
1145:      *                                 Defaults to the user's default calendar.
1146:      * @param boolean $showRecurrence  Return every instance of a recurring
1147:      *                                 event?  If false, will only return
1148:      *                                 recurring events once inside the
1149:      *                                 $startDate - $endDate range.
1150:      * @param boolean $alarmsOnly      Filter results for events with alarms.
1151:      *                                 Defaults to false.
1152:      * @param boolean $showRemote      Return events from remote calendars and
1153:      *                                 listTimeObject API as well?
1154:      *
1155:      * @param boolean $hideExceptions  Hide events that represent exceptions to
1156:      *                                 a recurring event (events with baseid
1157:      *                                 set)?
1158:      * @param boolean $coverDates      Add multi-day events to all dates?
1159:      *
1160:      * @return array  A list of event hashes.
1161:      * @throws Kronolith_Exception
1162:      */
1163:     public function listEvents($startstamp = null, $endstamp = null,
1164:                                $calendars = null, $showRecurrence = true,
1165:                                $alarmsOnly = false, $showRemote = true,
1166:                                $hideExceptions = false, $coverDates = true,
1167:                                $fetchTags = false)
1168:     {
1169:         if (!isset($calendars)) {
1170:             $calendars = array($GLOBALS['prefs']->getValue('default_share'));
1171:         } elseif (!is_array($calendars)) {
1172:             $calendars = array($calendars);
1173:         }
1174:         foreach ($calendars as &$calendar) {
1175:             $calendar = str_replace('internal_', '', $calendar);
1176:             if (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) {
1177:                 throw new Horde_Exception_PermissionDenied();
1178:             }
1179:         }
1180: 
1181:         return Kronolith::listEvents(
1182:             new Horde_Date($startstamp),
1183:             new Horde_Date($endstamp),
1184:             $calendars,
1185:             $showRecurrence,
1186:             $alarmsOnly,
1187:             $showRemote,
1188:             $hideExceptions,
1189:             $coverDates,
1190:             $fetchTags);
1191:     }
1192: 
1193:     /**
1194:      * Subscribe to a calendar.
1195:      *
1196:      * @param array $calendar  Calendar description hash, with required 'type'
1197:      *                         parameter. Currently supports 'http' and
1198:      *                         'webcal' for remote calendars.
1199:      *
1200:      * @throws Kronolith_Exception
1201:      */
1202:     public function subscribe($calendar)
1203:     {
1204:         if (!isset($calendar['type'])) {
1205:             throw new Kronolith_Exception(_("Unknown calendar protocol"));
1206:         }
1207: 
1208:         switch ($calendar['type']) {
1209:         case 'http':
1210:         case 'webcal':
1211:             Kronolith::subscribeRemoteCalendar($calendar);
1212:             break;
1213: 
1214:         case 'external':
1215:             $cals = unserialize($GLOBALS['prefs']->getValue('display_external_cals'));
1216:             if (array_search($calendar['name'], $cals) === false) {
1217:                 $cals[] = $calendar['name'];
1218:                 $GLOBALS['prefs']->setValue('display_external_cals', serialize($cals));
1219:             }
1220: 
1221:         default:
1222:             throw new Kronolith_Exception(_("Unknown calendar protocol"));
1223:         }
1224:     }
1225: 
1226:     /**
1227:      * Unsubscribe from a calendar.
1228:      *
1229:      * @param array $calendar  Calendar description array, with required 'type'
1230:      *                         parameter. Currently supports 'http' and
1231:      *                         'webcal' for remote calendars.
1232:      *
1233:      * @throws Kronolith_Exception
1234:      */
1235:     public function unsubscribe($calendar)
1236:     {
1237:         if (!isset($calendar['type'])) {
1238:             throw new Kronolith_Exception('Unknown calendar specification');
1239:         }
1240: 
1241:         switch ($calendar['type']) {
1242:         case 'http':
1243:         case 'webcal':
1244:             Kronolith::subscribeRemoteCalendar($calendar['url']);
1245:             break;
1246: 
1247:         case 'external':
1248:             $cals = unserialize($GLOBALS['prefs']->getValue('display_external_cals'));
1249:             if (($key = array_search($calendar['name'], $cals)) !== false) {
1250:                 unset($cals[$key]);
1251:                 $GLOBALS['prefs']->setValue('display_external_cals', serialize($cals));
1252:             }
1253: 
1254:         default:
1255:             throw new Kronolith_Exception('Unknown calendar specification');
1256:         }
1257:     }
1258: 
1259: 
1260:     /**
1261:      * Places an exclusive lock for a calendar or an event.
1262:      *
1263:      * @param string $calendar  The id of the calendar to lock
1264:      * @param string $event     The uid for the event to lock
1265:      *
1266:      * @return mixed   A lock ID on success, false if:
1267:      *                   - The calendar is already locked
1268:      *                   - The event is already locked
1269:      *                   - A calendar lock was requested and an event is
1270:      *                     already locked in the calendar
1271:      * @throws Kronolith_Exception
1272:      */
1273:     public function lock($calendar, $event = null)
1274:     {
1275:         if (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) {
1276:             throw new Horde_Exception_PermissionDenied();
1277:         }
1278:         if (!empty($event)) {
1279:             $uid = $calendar . ':' . $event;
1280:         }
1281: 
1282:         return $GLOBALS['kronolith_shares']->getShare($calendar)->lock($GLOBALS['injector']->getInstance('Horde_Lock'), $uid);
1283:     }
1284: 
1285:     /**
1286:      * Releases a lock.
1287:      *
1288:      * @param array $calendar  The event to lock.
1289:      * @param array $lockid    The lock id to unlock.
1290:      *
1291:      * @throws Kronolith_Exception
1292:      */
1293:     public function unlock($calendar, $lockid)
1294:     {
1295:         if (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) {
1296:             throw new Horde_Exception_PermissionDenied();
1297:         }
1298: 
1299:         return $GLOBALS['kronolith_shares']->getShare($calendar)->unlock($GLOBALS['injector']->getInstance('Horde_Lock'), $lockid);
1300:     }
1301: 
1302:     /**
1303:      * Check for existing calendar or event locks.
1304:      *
1305:      * @param array $calendar  The calendar to check locks for.
1306:      * @param array $event     The event to check locks for.
1307:      *
1308:      * @throws Kronolith_Exception
1309:      */
1310:     public function checkLocks($calendar, $event = null)
1311:     {
1312:         if (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) {
1313:             throw new Horde_Exception_PermissionDenied();
1314:         }
1315:         if (!empty($event)) {
1316:             $uid = $calendar . ':' . $event;
1317:         }
1318:         return $GLOBALS['kronolith_shares']->getShare($calendar)->checkLocks($GLOBALS['injector']->getInstance('Horde_Lock'), $uid);
1319:     }
1320: 
1321:     /**
1322:      *
1323:      * @return array  A list of calendars used to display free/busy information
1324:      */
1325:     public function getFbCalendars()
1326:     {
1327:         return (unserialize($GLOBALS['prefs']->getValue('fb_cals')));
1328:     }
1329: 
1330:     /**
1331:      * Retrieve the list of used tag_names, tag_ids and the total number
1332:      * of resources that are linked to that tag.
1333:      *
1334:      * @param array $tags   An optional array of tag_ids. If omitted, all tags
1335:      *                      will be included.
1336:      * @param string $user  Restrict result to those tagged by $user.
1337:      *
1338:      * @return array  An array containing tag_name, and total
1339:      */
1340:     public function listTagInfo($tags = null, $user = null)
1341:     {
1342:         return $GLOBALS['injector']
1343:             ->getInstance('Kronolith_Tagger')->getTagInfo($tags, 500, null, $user);
1344:     }
1345: 
1346:     /**
1347:      * SearchTags API:
1348:      * Returns an application-agnostic array (useful for when doing a tag search
1349:      * across multiple applications)
1350:      *
1351:      * The 'raw' results array can be returned instead by setting $raw = true.
1352:      *
1353:      * @param array $names           An array of tag_names to search for.
1354:      * @param integer $max           The maximum number of resources to return.
1355:      * @param integer $from          The number of the resource to start with.
1356:      * @param string $resource_type  The resource type [event, calendar, '']
1357:      * @param string $user           Restrict results to resources owned by $user.
1358:      * @param boolean $raw           Return the raw data?
1359:      *
1360:      * @return array An array of results:
1361:      * <pre>
1362:      *  'title'    - The title for this resource.
1363:      *  'desc'     - A terse description of this resource.
1364:      *  'view_url' - The URL to view this resource.
1365:      *  'app'      - The Horde application this resource belongs to.
1366:      * </pre>
1367:      */
1368:     public function searchTags($names, $max = 10, $from = 0,
1369:                                $resource_type = '', $user = null, $raw = false)
1370:     {
1371:         $results = $GLOBALS['injector']
1372:             ->getInstance('Kronolith_Tagger')
1373:             ->search(
1374:                 $names,
1375:                 array('type' => 'event', 'user' => $user));
1376: 
1377:         // Check for error or if we requested the raw data array.
1378:         if ($raw) {
1379:             return $results;
1380:         }
1381: 
1382:         $return = array();
1383:         if (!empty($results['events'])) {
1384:             foreach ($results['events'] as $event_id) {
1385:                 $driver = Kronolith::getDriver();
1386:                 $event = $driver->getByUid($event_id);
1387:                 $view_url = $event->getViewUrl();
1388:                 $return[] = array(
1389:                     'title' => $event->title,
1390:                     'desc'=> $event->start->strftime($GLOBALS['prefs']->getValue('date_format_mini')) . ' ' . $event->start->strftime($GLOBALS['prefs']->getValue('time_format')),
1391:                     'view_url' => $view_url,
1392:                     'app' => 'kronolith'
1393:                 );
1394:             }
1395:         }
1396: 
1397:         return $return;
1398:     }
1399: 
1400: }
1401: 
API documentation generated by ApiGen