'%application%/event.php?calendar=|calendar|&eventID=|event|&uid=|uid|' ); /** * Returns the share helper prefix * * @return string */ public function shareHelp() { return 'shares'; } /** * Returns the last modification timestamp for the given uid. * * @param string $uid The uid to look for. * @param string $calendar The calendar to search in. * * @return integer The timestamp for the last modification of $uid. */ public function modified($uid, $calendar = null) { $modified = $this->getActionTimestamp($uid, 'modify', $calendar); if (empty($modified)) { $modified = $this->getActionTimestamp($uid, 'add', $calendar); } return $modified; } /** * Browse through Kronolith's object tree. * * @param string $path The level of the tree to browse. * @param array $properties The item properties to return. Defaults to 'name', * 'icon', and 'browseable'. * * @return array The contents of $path * @throws Kronolith_Exception */ public function browse($path = '', $properties = array()) { global $injector, $registry; // Default properties. if (!$properties) { $properties = array('name', 'icon', 'browseable'); } if (substr($path, 0, 9) == 'kronolith') { $path = substr($path, 9); } $path = trim($path, '/'); $parts = explode('/', $path); $currentUser = $registry->getAuth(); if (empty($path)) { // This request is for a list of all users who have calendars // visible to the requesting user. $calendars = Kronolith::listInternalCalendars(false, Horde_Perms::READ); $owners = array(); foreach ($calendars as $calendar) { $owners[$calendar->get('owner') ? $calendar->get('owner') : '-system-'] = true; } $results = array(); foreach (array_keys($owners) as $owner) { $path = 'kronolith/' . $registry->convertUsername($owner, false); if (in_array('name', $properties)) { $results[$path]['name'] = $injector ->getInstance('Horde_Core_Factory_Identity') ->create($owner) ->getName(); } if (in_array('icon', $properties)) { $results[$path]['icon'] = Horde_Themes::img('user.png'); } if (in_array('browseable', $properties)) { $results[$path]['browseable'] = true; } } return $results; } elseif (count($parts) == 1) { // This request is for all calendars owned by the requested user $owner = $parts[0] == '-system-' ? '' : $registry->convertUsername($parts[0], true); $calendars = $injector->getInstance('Kronolith_Shares') ->listShares( $currentUser, array('perm' => Horde_Perms::SHOW, 'attributes' => $owner) ); $results = array(); foreach ($calendars as $calendarId => $calendar) { if ($parts[0] == '-system-' && $calendar->get('owner')) { continue; } $retpath = 'kronolith/' . $parts[0] . '/' . $calendarId; if (in_array('name', $properties)) { $results[$retpath]['name'] = sprintf(_("Events from %s"), Kronolith::getLabel($calendar)); $results[$retpath . '.ics']['name'] = Kronolith::getLabel($calendar); } if (in_array('displayname', $properties)) { $results[$retpath]['displayname'] = Kronolith::getLabel($calendar); $results[$retpath . '.ics']['displayname'] = Kronolith::getLabel($calendar) . '.ics'; } if (in_array('owner', $properties)) { $results[$retpath]['owner'] = $results[$retpath . '.ics']['owner'] = $calendar->get('owner') ? $registry->convertUsername($calendar->get('owner'), false) : '-system-'; } if (in_array('icon', $properties)) { $results[$retpath]['icon'] = Horde_Themes::img('kronolith.png'); $results[$retpath . '.ics']['icon'] = Horde_Themes::img('mime/icalendar.png'); } if (in_array('browseable', $properties)) { $results[$retpath]['browseable'] = $calendar->hasPermission($currentUser, Horde_Perms::READ); $results[$retpath . '.ics']['browseable'] = false; } if (in_array('read-only', $properties)) { $results[$retpath]['read-only'] = $results[$retpath . '.ics']['read-only'] = !$calendar->hasPermission($currentUser, Horde_Perms::EDIT); } if (in_array('contenttype', $properties)) { $results[$retpath . '.ics']['contenttype'] = 'text/calendar'; } } return $results; } elseif (count($parts) == 2 && array_key_exists($parts[1], Kronolith::listInternalCalendars(false, Horde_Perms::READ))) { // This request is browsing into a specific calendar. Generate // the list of items and represent them as files within the // directory. try { $calendar = $injector->getInstance('Kronolith_Shares') ->getShare($parts[1]); } catch (Horde_Exception_NotFound $e) { throw new Kronolith_Exception(_("Invalid calendar requested."), 404); } catch (Horde_Share_Exception $e) { throw new Kronolith_Exception($e->getMessage, 500); } $kronolith_driver = Kronolith::getDriver(null, $parts[1]); $events = $kronolith_driver->listEvents(); $icon = Horde_Themes::img('mime/icalendar.png'); $owner = $calendar->get('owner') ? $registry->convertUsername($calendar->get('owner'), false) : '-system-'; $results = array(); foreach ($events as $dayevents) { foreach ($dayevents as $event) { $key = 'kronolith/' . $path . '/' . $event->id; if (in_array('name', $properties)) { $results[$key]['name'] = $event->getTitle(); } if (in_array('owner', $properties)) { $results[$key]['owner'] = $owner; } if (in_array('icon', $properties)) { $results[$key]['icon'] = $icon; } if (in_array('browseable', $properties)) { $results[$key]['browseable'] = false; } if (in_array('contenttype', $properties)) { $results[$key]['contenttype'] = 'text/calendar'; } if (in_array('modified', $properties)) { $results[$key]['modified'] = $this->modified($event->uid, $parts[1]); } if (in_array('created', $properties)) { $results[$key]['created'] = $this->getActionTimestamp($event->uid, 'add'); } } } return $results; } else { // The only valid request left is for either a specific event or // for the entire calendar. if (count($parts) == 3 && array_key_exists($parts[1], Kronolith::listInternalCalendars(false, Horde_Perms::READ))) { // This request is for a specific item within a given calendar. $event = Kronolith::getDriver(null, $parts[1])->getEvent($parts[2]); $result = array( 'data' => $this->export($event->uid, 'text/calendar'), 'mimetype' => 'text/calendar'); $modified = $this->modified($event->uid, $parts[1]); if (!empty($modified)) { $result['mtime'] = $modified; } return $result; } elseif (count($parts) == 2 && substr($parts[1], -4, 4) == '.ics' && array_key_exists(substr($parts[1], 0, -4), Kronolith::listInternalCalendars(false, Horde_Perms::READ))) { // This request is for an entire calendar (calendar.ics). $ical_data = $this->exportCalendar(substr($parts[1], 0, -4), 'text/calendar'); return array( 'data' => $ical_data, 'mimetype' => 'text/calendar', 'contentlength' => strlen($ical_data), 'mtime' => $_SERVER['REQUEST_TIME'] ); } else { // All other requests are a 404: Not Found return false; } } } /** * Saves a file into the Kronolith tree. * * @param string $path The path where to PUT the file. * @param string $content The file content. * @param string $content_type The file's content type. * * @return array The event UIDs. * @throws Kronolith_Exception */ public function put($path, $content, $content_type) { if (substr($path, 0, 9) == 'kronolith') { $path = substr($path, 9); } $path = trim($path, '/'); $parts = explode('/', $path); if (count($parts) == 2 && substr($parts[1], -4) == '.ics') { // Workaround for WebDAV clients that are not smart enough to send // the right content type. Assume text/calendar. if ($content_type == 'application/octet-stream') { $content_type = 'text/calendar'; } $calendar = substr($parts[1], 0, -4); } elseif (count($parts) == 3) { $calendar = $parts[1]; // Workaround for WebDAV clients that are not smart enough to send // the right content type. Assume text/calendar. if ($content_type == 'application/octet-stream') { $content_type = 'text/calendar'; } } else { throw new Kronolith_Exception(_("Invalid calendar data supplied.")); } if (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) { // FIXME: Should we attempt to create a calendar based on the // filename in the case that the requested calendar does not // exist? throw new Kronolith_Exception(_("Calendar does not exist or no permission to edit")); } // Store all currently existings UIDs. Use this info to delete UIDs not // present in $content after processing. $ids = array(); if (count($parts) == 2) { $uids_remove = array_flip($this->listUids($calendar)); } else { $uids_remove = array(); } switch ($content_type) { case 'text/calendar': case 'text/x-vcalendar': $iCal = new Horde_Icalendar(); if (!($content instanceof Horde_Icalendar_Vevent)) { if (!$iCal->parsevCalendar($content)) { throw new Kronolith_Exception(_("There was an error importing the iCalendar data.")); } } else { $iCal->addComponent($content); } $kronolith_driver = Kronolith::getDriver(); $kronolith_driver->open($parts[1]); $history = $GLOBALS['injector']->getInstance('Horde_History'); foreach ($iCal->getComponents() as $content) { if ($content instanceof Horde_Icalendar_Vevent) { $event = $kronolith_driver->getEvent(); $event->fromiCalendar($content); $uid = $event->uid; if ($uid) { // Remove from uids_remove list so we won't delete in // the end. unset($uids_remove[$uid]); try { $existing_event = $kronolith_driver->getByUID( $uid, array($calendar) ); // Check if our event is newer then the existing - // get the event's history. $created = $modified = null; try { $created = $history->getActionTimestamp( 'kronolith:' . $calendar . ':' . $uid, 'add' ); $modified = $history->getActionTimestamp( 'kronolith:' . $calendar . ':' . $uid, 'modify' ); /* The history driver returns 0 for not * found. If 0 or null does not matter, strip * this */ if ($created == 0) { $created = null; } if ($modified == 0) { $modified = null; } } catch (Horde_Exception $e) { } if (empty($modified) && !empty($created)) { $modified = $created; } try { if (!empty($modified) && $modified >= $content->getAttribute('LAST-MODIFIED')) { // LAST-MODIFIED timestamp of existing // entry is newer: don't replace it. continue; } } catch (Horde_Icalendar_Exception $e) { } // Don't change creator/owner. $event->creator = $existing_event->creator; } catch (Horde_Exception_NotFound $e) { } } // Save entry. $event->save(); $ids[] = $event->uid; } } break; default: throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $content_type)); } if (Kronolith::hasPermission($calendar, Horde_Perms::DELETE)) { foreach (array_keys($uids_remove) as $uid) { $this->delete($uid); } } return $ids; } /** * Deletes a file from the Kronolith tree. * * @param string $path The path to the file. * * @throws Kronolith_Exception */ public function path_delete($path) { if (substr($path, 0, 9) == 'kronolith') { $path = substr($path, 9); } $path = trim($path, '/'); $parts = explode('/', $path); if (substr($parts[1], -4) == '.ics') { $calendarId = substr($parts[1], 0, -4); } else { $calendarId = $parts[1]; } if (!(count($parts) == 2 || count($parts) == 3) || !Kronolith::hasPermission($calendarId, Horde_Perms::DELETE)) { throw new Kronolith_Exception(_("Calendar does not exist or no permission to delete")); } if (count($parts) == 3) { // Delete just a single entry Kronolith::getDriver(null, $calendarId)->deleteEvent($parts[2]); } else { // Delete the entire calendar try { Kronolith::getDriver()->delete($calendarId); // Remove share and all groups/permissions. $kronolith_shares = $GLOBALS['injector']->getInstance('Kronolith_Shares'); $share = $kronolith_shares->getShare($calendarId); $kronolith_shares->removeShare($share); } catch (Exception $e) { throw new Kronolith_Exception(sprintf(_("Unable to delete calendar \"%s\": %s"), $calendarId, $e->getMessage())); } } } /** * Returns all calendars a user has access to, according to several * parameters/permission levels. * * @param boolean $owneronly Only return calendars that this user owns? * Defaults to false. * @param integer $permission The permission to filter calendars by. * * @return array The calendar list. */ public function listCalendars($owneronly = false, $permission = null) { if (is_null($permission)) { $permission = Horde_Perms::SHOW; } return array_keys(Kronolith::listInternalCalendars($owneronly, $permission)); } /** * Returns a list of available sources. * * @param boolean $writeable If true, limits to writeable sources. * @param boolean $sync_only Only include syncable sources. * * @return array An array of the available sources. Keys are source IDs, * values are source titles. * @since 4.2.0 */ public function sources($writeable = false, $sync_only = false) { $out = array(); foreach (Kronolith::listInternalCalendars(false, $writeable ? Horde_Perms::EDIT : Horde_Perms::READ) as $id => $data) { $out[$id] = $data->get('name'); } if ($sync_only) { $syncable = Kronolith::getSyncCalendars(); $out = array_intersect_key($out, array_flip($syncable)); } return $out; } /** * Retrieve the UID for the current user's default calendar. * * @return string UID. * @since 4.2.0 */ public function getDefaultShare() { return Kronolith::getDefaultCalendar(Horde_Perms::EDIT, true); } /** * Returns the ids of all the events that happen within a time period. * Only includes recurring events once per time period, and does not include * events that represent exceptions, making this method useful for syncing * purposes. For more control, use the listEvents method. * * @param string|array $calendars The calendar to check for events. * @param object $startstamp The start of the time range. * @param object $endstamp The end of the time range. * * @return array The event ids happening in this time period. * @throws Kronolith_Exception */ public function listUids($calendars = null, $startstamp = 0, $endstamp = 0) { if (empty($calendars)) { $calendars = Kronolith::getSyncCalendars(); } elseif (!is_array($calendars)) { $calendars = array($calendars); } $driver = Kronolith::getDriver(); $results = array(); foreach ($calendars as $calendar) { if (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) { Horde::log(sprintf( _("Permission Denied or Calendar Not Found: %s - skipping."), $calendar)); continue; } try { $driver->open($calendar); $events = $driver->listEvents( $startstamp ? new Horde_Date($startstamp) : null, $endstamp ? new Horde_Date($endstamp) : null, array('cover_dates' => false, 'hide_exceptions' => true) ); Kronolith::mergeEvents($results, $events); } catch (Kronolith_Exception $e) { Horde::log($e); } } $uids = array(); foreach ($results as $dayevents) { foreach ($dayevents as $event) { $uids[] = $event->uid; } } return $uids; } /** * Returns an array of UIDs for events that have had $action happen since * $timestamp. * * @param string $action The action to check for - add, modify, or delete. * @param integer $timestamp The time to start the search. * @param string $calendar The calendar to search in. * @param integer $end The optional ending timestamp * @param boolean $isModSeq If true, $timestamp and $end are modification * sequences and not timestamps. @since 4.1.1 * * @return array An array of UIDs matching the action and time criteria. * * @throws Kronolith_Exception * @throws Horde_History_Exception * @throws InvalidArgumentException */ public function listBy($action, $timestamp, $calendar = null, $end = null, $isModSeq = false) { if (empty($calendar)) { $cs = Kronolith::getSyncCalendars($action == 'delete'); $results = array(); foreach ($cs as $c) { $results = array_merge( $results, $this->listBy($action, $timestamp, $c, $end, $isModSeq)); } return $results; } $filter = array(array('op' => '=', 'field' => 'action', 'value' => $action)); if (!empty($end) && !$isModSeq) { $filter[] = array('op' => '<', 'field' => 'ts', 'value' => $end); } if (!$isModSeq) { $histories = $GLOBALS['injector'] ->getInstance('Horde_History') ->getByTimestamp('>', $timestamp, $filter, 'kronolith:' . $calendar); } else { $histories = $GLOBALS['injector'] ->getInstance('Horde_History') ->getByModSeq($timestamp, $end, $filter, 'kronolith:' . $calendar); } // Strip leading kronolith:username:. return preg_replace('/^([^:]*:){2}/', '', array_keys($histories)); } /** * Method for obtaining all server changes between two timestamps. Basically * a wrapper around listBy(), but returns an array containing all adds, * edits and deletions. If $ignoreExceptions is true, events representing * recurring event exceptions will not be included in the results. * * @param integer $start The starting timestamp * @param integer $end The ending timestamp. * @param boolean $ignoreExceptions Do not include exceptions in results. * @param boolean $isModSeq If true, $timestamp and $end are * modification sequences and not * timestamps. @since 4.1.1 * @param string|array $calendars The sources to check. @since 4.2.0 * * @return array An hash with 'add', 'modify' and 'delete' arrays. * @throws Horde_Exception_PermissionDenied * @throws Kronolith_Exception */ public function getChanges( $start, $end, $ignoreExceptions = true, $isModSeq = false, $calendars = null) { // Only get the calendar once if (is_null($calendars)) { $cs = Kronolith::getSyncCalendars(); } else { if (!is_array($calendars)) { $calendars = array($calendars); } $cs = $calendars; } $changes = array( 'add' => array(), 'modify' => array(), 'delete' => array()); foreach ($cs as $c) { // New events $uids = $this->listBy('add', $start, $c, $end, $isModSeq); if ($ignoreExceptions) { foreach ($uids as $uid) { try { $event = Kronolith::getDriver()->getByUID($uid, array($c)); } catch (Exception $e) { continue; } if (empty($event->baseid)) { $changes['add'][] = $uid; } } } else { $changes['add'] = array_keys(array_flip(array_merge($changes['add'], $uids))); } // Edits $uids = $this->listBy('modify', $start, $c, $end, $isModSeq); if ($ignoreExceptions) { foreach ($uids as $uid) { try { $event = Kronolith::getDriver()->getByUID($uid, array($c)); } catch (Exception $e) { continue; } if (empty($event->baseid)) { $changes['modify'][] = $uid; } } } else { $changes['modify'] = array_keys(array_flip(array_merge($changes['modify'], $uids))); } // No way to figure out if this was an exception, so we must include all $changes['delete'] = array_keys( array_flip(array_merge($changes['delete'], $this->listBy('delete', $start, $c, $end, $isModSeq)))); } return $changes; } /** * Return all changes occuring between the specified modification * sequences. * * @param integer $start The starting modseq. * @param integer $end The ending modseq. * @param string|array $calendars The sources to check. @since 4.2.0 * * @return array The changes @see getChanges() * @since 4.1.1 */ public function getChangesByModSeq($start, $end, $calendars = null) { return $this->getChanges($start, $end, true, true, $calendars); } /** * Returns the timestamp of an operation for a given uid an action * * @param string $uid The uid to look for. * @param string $action The action to check for - add, modify, or delete. * @param string $calendar The calendar to search in. * @param boolean $modSeq Request a modification sequence instead of a * timestamp. @since 4.1.1 * * @return integer The timestamp or modseq for this action. * * @throws Kronolith_Exception * @throws InvalidArgumentException */ public function getActionTimestamp($uid, $action, $calendar = null, $modSeq = false) { if (empty($calendar)) { $calendar = Kronolith::getDefaultCalendar(); } elseif (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) { throw new Horde_Exception_PermissionDenied(); } if (!$modSeq) { return $GLOBALS['injector']->getInstance('Horde_History')->getActionTimestamp('kronolith:' . $calendar . ':' . $uid, $action); } return $GLOBALS['injector']->getInstance('Horde_History')->getActionModSeq('kronolith:' . $calendar . ':' . $uid, $action); } /** * Return the largest modification sequence from the history backend. * * @param string $id The calendar id to return the hightest MDOSEQ for. If * null, the highest MODSEQ across all calendars is * returned. @since 4.2.0 * * @return integer The MODSEQ value. * @since 4.1.1 */ public function getHighestModSeq($id = null) { $parent = 'kronolith'; if (!empty($id)) { $parent .= ':' . $id; } return $GLOBALS['injector']->getInstance('Horde_History')->getHighestModSeq($parent); } /** * Imports an event represented in the specified content type. * * @param string $content The content of the event. * @param string $contentType What format is the data in? Currently supports: *
* text/calendar
* text/x-vcalendar
* activesync
*
* @param string $calendar What calendar should the event be added to?
*
* @return array The event's UID.
* @throws Kronolith_Exception
*/
public function import($content, $contentType, $calendar = null)
{
if (!isset($calendar)) {
$calendar = Kronolith::getDefaultCalendar(Horde_Perms::EDIT);
} elseif (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
$kronolith_driver = Kronolith::getDriver(null, $calendar);
switch ($contentType) {
case 'text/calendar':
case 'text/x-vcalendar':
$iCal = new Horde_Icalendar();
if (!($content instanceof Horde_Icalendar_Vevent)) {
if (!$iCal->parsevCalendar($content)) {
throw new Kronolith_Exception(_("There was an error importing the iCalendar data."));
}
} else {
$iCal->addComponent($content);
}
$ical_importer = new Kronolith_Icalendar_Handler_Base($iCal, $kronolith_driver);
$result = array_flip($ical_importer->process());
return current($result);
case 'activesync':
$event = $kronolith_driver->getEvent();
$event->fromASAppointment($content);
$event->save();
return $event->uid;
}
throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
/**
* Imports a single vEvent part to storage.
*
* @param Horde_Icalendar_Vevent $content The vEvent part
* @param Kronolith_Driver $driver The kronolith driver
* @param boolean $exception Content represents an exception
* in a recurrence series.
*
* @return string The new event's uid
*/
protected function _addiCalEvent($content, $driver, $exception = false)
{
$event = $driver->getEvent();
$event->fromiCalendar($content, true);
// Check if the entry already exists in the data source, first by UID.
if (!$exception) {
try {
$driver->getByUID($event->uid, array($driver->calendar));
throw new Kronolith_Exception(sprintf(_("%s Already Exists"), $event->uid));
} catch (Horde_Exception $e) {}
}
$result = $driver->search($event);
// Check if the match really is an exact match:
foreach ($result as $days) {
foreach ($days as $match) {
if ($match->start == $event->start &&
$match->end == $event->end &&
$match->title == $event->title &&
$match->location == $event->location &&
$match->hasPermission(Horde_Perms::EDIT)) {
throw new Kronolith_Exception(sprintf(_("%s Already Exists"), $match->uid));
}
}
}
$event->save();
return $event->uid;
}
/**
* Imports an event parsed from a string.
*
* @param string $text The text to parse into an event
* @param string $calendar The calendar into which the event will be
* imported. If 'null', the user's default
* calendar will be used.
*
* @return array The UID of all events that were added.
* @throws Kronolith_Exception
*/
public function quickAdd($text, $calendar = null)
{
if (!isset($calendar)) {
$calendar = Kronolith::getDefaultCalendar(Horde_Perms::EDIT);
} elseif (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
$event = Kronolith::quickAdd($text, $calendar);
return $event->uid;
}
/**
* Exports an event, identified by UID, in the requested content type.
*
* @param string $uid Identify the event to export.
* @param string $contentType What format should the data be in?
* A string with one of:
*
* text/calendar (VCALENDAR 2.0. Recommended as
* this is specified in rfc2445)
* text/x-vcalendar (old VCALENDAR 1.0 format.
* Still in wide use)
* activesync (Horde_ActiveSync_Message_Appointment)
*
* @param array $options Any additional options to be passed to the
* exporter.
* @param array $calendars Require event to be in these calendars.
* @since 4.2.0
*
* @return string The requested data.
* @throws Kronolith_Exception
* @throws Horde_Exception_NotFound
*/
public function export($uid, $contentType, array $options = array(), array $calendars = null)
{
$event = Kronolith::getDriver()->getByUID($uid, $calendars);
if (!$event->hasPermission(Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied();
}
$version = '2.0';
switch ($contentType) {
case 'text/x-vcalendar':
$version = '1.0';
case 'text/calendar':
$share = $GLOBALS['injector']->getInstance('Kronolith_Shares')->getShare($event->calendar);
$iCal = new Horde_Icalendar($version);
$iCal->setAttribute('X-WR-CALNAME', $share->get('name'));
// Create a new vEvent.
$iCal->addComponent($event->toiCalendar($iCal));
return $iCal->exportvCalendar();
case 'activesync':
return $event->toASAppointment($options);
}
throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
/**
* Exports a calendar in the requested content type.
*
* @param string $calendar The calendar to export.
* @param string $contentType What format should the data be in?
* A string with one of:
*
* text/calendar (VCALENDAR 2.0. Recommended as
* this is specified in rfc2445)
* text/x-vcalendar (old VCALENDAR 1.0 format.
* Still in wide use)
*
*
* @return string The iCalendar representation of the calendar.
* @throws Kronolith_Exception
*/
public function exportCalendar($calendar, $contentType)
{
if (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied();
}
$kronolith_driver = Kronolith::getDriver(null, $calendar);
$events = $kronolith_driver->listEvents(null, null, array(
'cover_dates' => false,
'hide_exceptions' => true)
);
$version = '2.0';
switch ($contentType) {
case 'text/x-vcalendar':
$version = '1.0';
case 'text/calendar':
$share = $GLOBALS['injector']
->getInstance('Kronolith_Shares')
->getShare($calendar);
$iCal = new Horde_Icalendar($version);
$iCal->setAttribute('X-WR-CALNAME', $share->get('name'));
if (strlen($share->get('desc'))) {
$iCal->setAttribute('X-WR-CALDESC', $share->get('desc'));
}
foreach ($events as $dayevents) {
foreach ($dayevents as $event) {
$iCal->addComponent($event->toiCalendar($iCal));
}
}
return $iCal->exportvCalendar();
}
throw new Kronolith_Exception(sprintf(
_("Unsupported Content-Type: %s"),
$contentType)
);
}
/**
* Deletes an event identified by UID.
*
* @param string|array $uid A single UID or an array identifying the
* event(s) to delete.
*
* @param string $recurrenceId The reccurenceId for the event instance, if
* this is a deletion of a recurring event
* instance ($uid must not be an array).
* @param string $range The range value if deleting a recurring
* event instance. Only supported values are
* null or Kronolith::RANGE_THISANDFUTURE.
* @since 4.1.5
*
* @throws Kronolith_Exception
*/
public function delete($uid, $recurrenceId = null, $range = null)
{
// Handle an array of UIDs for convenience of deleting multiple events
// at once.
if (is_array($uid)) {
foreach ($uid as $g) {
$this->delete($g);
}
return;
}
$kronolith_driver = Kronolith::getDriver();
$events = $kronolith_driver->getByUID($uid, null, true);
$event = null;
// First try the user's own calendars.
if (empty($event)) {
$ownerCalendars = Kronolith::listInternalCalendars(true, Horde_Perms::DELETE);
foreach ($events as $ev) {
if (isset($ownerCalendars[$ev->calendar])) {
$kronolith_driver->open($ev->calendar);
$event = $ev;
break;
}
}
}
// If not successful, try all calendars the user has access to.
if (empty($event)) {
$deletableCalendars = Kronolith::listInternalCalendars(false, Horde_Perms::DELETE);
foreach ($events as $ev) {
if (isset($deletableCalendars[$ev->calendar])) {
$kronolith_driver->open($ev->calendar);
$event = $ev;
break;
}
}
}
// Are we an admin cleaing up user data?
if (empty($event) && $GLOBALS['registry']->isAdmin()) {
$event = $events[0];
}
if (empty($event)) {
throw new Horde_Exception_PermissionDenied();
}
if ($recurrenceId && $event->recurs() && empty($range)) {
$deleteDate = new Horde_Date($recurrenceId);
$event->recurrence->addException($deleteDate->format('Y'), $deleteDate->format('m'), $deleteDate->format('d'));
$event->save();
} elseif ($range == Kronolith::RANGE_THISANDFUTURE) {
// Deleting the instance and remaining series.
$instance = new Horde_Date($recurrenceId);
$recurEnd = clone($instance);
$recurEnd->mday--;
if ($event->end->compareDate($recurEnd) > 0) {
$kronolith_driver->deleteEvent($event->id);
} else {
$event->recurrence->setRecurEnd($recurEnd);
$result = $event->save();
}
} elseif ($recurrenceId) {
throw new Kronolith_Exception(_("Unable to delete event. An exception date was provided but the event does not seem to be recurring."));
} else {
$kronolith_driver->deleteEvent($event->id);
}
}
/**
* Replaces the event identified by UID with the content represented in the
* specified contentType.
*
* @param string $uid Idenfity the event to replace.
* @param mixed $content The content of the event. String or
* Horde_Icalendar_Vevent
* @param string $contentType What format is the data in? Currently supports:
* text/calendar
* text/x-vcalendar
* (Ignored if content is Horde_Icalendar_Vevent)
* activesync (Horde_ActiveSync_Message_Appointment)
* @param string $calendar Ensure the event is replaced in the specified
* calendar. @since 4.2.0
*
* @throws Kronolith_Exception
*/
public function replace($uid, $content, $contentType, $calendar = null)
{
$event = Kronolith::getDriver(null, $calendar)->getByUID($uid);
if (!$event->hasPermission(Horde_Perms::EDIT) ||
($event->private && $event->creator != $GLOBALS['registry']->getAuth())) {
throw new Horde_Exception_PermissionDenied();
}
if ($content instanceof Horde_Icalendar_Vevent) {
$component = $content;
} elseif ($content instanceof Horde_ActiveSync_Message_Appointment) {
$event->fromASAppointment($content);
$event->save();
$event->uid = $uid;
return;
} else {
switch ($contentType) {
case 'text/calendar':
case 'text/x-vcalendar':
if (!($content instanceof Horde_Icalendar_Vevent)) {
$iCal = new Horde_Icalendar();
if (!$iCal->parsevCalendar($content)) {
throw new Kronolith_Exception(_("There was an error importing the iCalendar data."));
}
$components = $iCal->getComponents();
$component = null;
foreach ($components as $content) {
if ($content instanceof Horde_Icalendar_Vevent) {
if ($component !== null) {
throw new Kronolith_Exception(_("Multiple iCalendar components found; only one vEvent is supported."));
}
$component = $content;
}
}
if ($component === null) {
throw new Kronolith_Exception(_("No iCalendar data was found."));
}
}
break;
default:
throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
}
try {
$component->getAttribute('RECURRENCE-ID');
$this->_addiCalEvent($component, Kronolith::getDriver(null, $calendar), true);
} catch (Horde_Icalendar_Exception $e) {
$event->fromiCalendar($component, true);
// Ensure we keep the original UID, even when content does not
// contain one and fromiCalendar creates a new one.
$event->uid = $uid;
$event->save();
}
}
/**
* Generates free/busy information for a given time period.
*
* @param integer $startstamp The start of the time period to retrieve.
* @param integer $endstamp The end of the time period to retrieve.
* @param string $calendar The calendar to view free/busy slots for.
* Defaults to the user's default calendar.
*
* @return Horde_Icalendar_Vfreebusy A freebusy object that covers the
* specified time period.
* @throws Kronolith_Exception
*/
public function getFreeBusy($startstamp = null, $endstamp = null,
$calendar = null)
{
if (is_null($calendar)) {
$calendar = Kronolith::getDefaultCalendar();
}
// Free/Busy information is globally available; no permission
// check is needed.
return Kronolith_FreeBusy::generate($calendar, $startstamp, $endstamp, true);
}
/**
* Attempt to lookup the free/busy information for the given email address.
*
* @param string $email The email to lookup free/busy information for.
* @param boolean $json Return the data in a simple json format. If false,
* returns the vCalander object.
* @since 4.1.0
*/
public function lookupFreeBusy($email, $json = false)
{
return Kronolith_FreeBusy::get($email, $json);
}
/**
* Retrieves a Kronolith_Event object, given an event UID.
*
* @param string $uid The event's UID.
*
* @return Kronolith_Event A valid Kronolith_Event.
* @throws Kronolith_Exception
*/
public function eventFromUID($uid)
{
$event = Kronolith::getDriver()->getByUID($uid);
if (!$event->hasPermission(Horde_Perms::SHOW)) {
throw new Horde_Exception_PermissionDenied();
}
return $event;
}
/**
* Updates an attendee's response status for a specified event.
*
* @param Horde_Icalendar_Vevent $response A Horde_Icalendar_Vevent
* object, with a valid UID
* attribute that points to an
* existing event. This is
* typically the vEvent portion
* of an iTip meeting-request
* response, with the attendee's
* response in an ATTENDEE
* parameter.
* @param string $sender The email address of the
* person initiating the
* update. Attendees are only
* updated if this address
* matches.
*
* @throws Kronolith_Exception
*/
public function updateAttendee($response, $sender = null)
{
try {
$uid = $response->getAttribute('UID');
} catch (Horde_Icalendar_Exception $e) {
throw new Kronolith_Exception($e);
}
$events = Kronolith::getDriver()->getByUID($uid, null, true);
/* First try the user's own calendars. */
$ownerCalendars = Kronolith::listInternalCalendars(true, Horde_Perms::EDIT);
$event = null;
foreach ($events as $ev) {
if (isset($ownerCalendars[$ev->calendar])) {
$event = $ev;
break;
}
}
/* If not successful, try all calendars the user has access to. */
if (empty($event)) {
$editableCalendars = Kronolith::listInternalCalendars(false, Horde_Perms::EDIT);
foreach ($events as $ev) {
if (isset($editableCalendars[$ev->calendar])) {
$event = $ev;
break;
}
}
}
if (empty($event) ||
($event->private && $event->creator != $GLOBALS['registry']->getAuth())) {
throw new Horde_Exception_PermissionDenied();
}
$atnames = $response->getAttribute('ATTENDEE');
if (!is_array($atnames)) {
$atnames = array($atnames);
}
$atparms = $response->getAttribute('ATTENDEE', true);
$found = false;
$error = _("No attendees have been updated because none of the provided email addresses have been found in the event's attendees list.");
foreach ($atnames as $index => $attendee) {
if ($response->getAttribute('VERSION') < 2) {
$addr_ob = new Horde_Mail_Rfc822_Address($attendee);
if (!$addr_ob->valid) {
continue;
}
$attendee = $addr_ob->bare_address;
$name = $addr_ob->personal;
} else {
$attendee = str_ireplace('mailto:', '', $attendee);
$name = isset($atparms[$index]['CN']) ? $atparms[$index]['CN'] : null;
}
if ($event->hasAttendee($attendee)) {
if (is_null($sender) || $sender == $attendee) {
$event->addAttendee($attendee, Kronolith::PART_IGNORE, Kronolith::responseFromICal($atparms[$index]['PARTSTAT']), $name);
$found = true;
} else {
$error = _("The attendee hasn't been updated because the update was not sent from the attendee.");
}
}
}
$event->save();
if (!$found) {
throw new Kronolith_Exception($error);
}
}
/**
* Lists events for a given time period.
*
* @param integer $startstamp The start of the time period to
* retrieve.
* @param integer $endstamp The end of the time period to retrieve.
* @param array $calendars The calendars to view events from.
* Defaults to the user's default calendar.
* @param boolean $showRecurrence Return every instance of a recurring
* event? If false, will only return
* recurring events once inside the
* $startDate - $endDate range.
* @param boolean $alarmsOnly Filter results for events with alarms.
* Defaults to false.
* @param boolean $showRemote Return events from remote calendars and
* listTimeObject API as well?
*
* @param boolean $hideExceptions Hide events that represent exceptions to
* a recurring event (events with baseid
* set)?
* @param boolean $coverDates Add multi-day events to all dates?
*
* @return array A list of event hashes.
* @throws Kronolith_Exception
*/
public function listEvents($startstamp = null, $endstamp = null,
$calendars = null, $showRecurrence = true,
$alarmsOnly = false, $showRemote = true,
$hideExceptions = false, $coverDates = true,
$fetchTags = false)
{
if (!isset($calendars)) {
$calendars = array($GLOBALS['prefs']->getValue('default_share'));
} elseif (!is_array($calendars)) {
$calendars = array($calendars);
}
foreach ($calendars as &$calendar) {
$calendar = str_replace('internal_', '', $calendar);
if (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied();
}
}
return Kronolith::listEvents(
new Horde_Date($startstamp),
new Horde_Date($endstamp),
$calendars, array(
'show_recurrence' => $showRecurrence,
'has_alarm' => $alarmsOnly,
'show_remote' => $showRemote,
'hide_exceptions' => $hideExceptions,
'cover_dates' => $coverDates,
'fetch_tags' => $fetchTags)
);
}
/**
* Subscribe to a calendar.
*
* @param array $calendar Calendar description hash, with required 'type'
* parameter. Currently supports 'http' and
* 'webcal' for remote calendars.
*
* @throws Kronolith_Exception
*/
public function subscribe($calendar)
{
if (!isset($calendar['type'])) {
throw new Kronolith_Exception(_("Unknown calendar protocol"));
}
switch ($calendar['type']) {
case 'http':
case 'webcal':
Kronolith::subscribeRemoteCalendar($calendar);
break;
case 'external':
$cals = unserialize($GLOBALS['prefs']->getValue('display_external_cals'));
if (array_search($calendar['name'], $cals) === false) {
$cals[] = $calendar['name'];
$GLOBALS['prefs']->setValue('display_external_cals', serialize($cals));
}
default:
throw new Kronolith_Exception(_("Unknown calendar protocol"));
}
}
/**
* Unsubscribe from a calendar.
*
* @param array $calendar Calendar description array, with required 'type'
* parameter. Currently supports 'http' and
* 'webcal' for remote calendars.
*
* @throws Kronolith_Exception
*/
public function unsubscribe($calendar)
{
if (!isset($calendar['type'])) {
throw new Kronolith_Exception('Unknown calendar specification');
}
switch ($calendar['type']) {
case 'http':
case 'webcal':
Kronolith::subscribeRemoteCalendar($calendar['url']);
break;
case 'external':
$cals = unserialize($GLOBALS['prefs']->getValue('display_external_cals'));
if (($key = array_search($calendar['name'], $cals)) !== false) {
unset($cals[$key]);
$GLOBALS['prefs']->setValue('display_external_cals', serialize($cals));
}
default:
throw new Kronolith_Exception('Unknown calendar specification');
}
}
/**
* Places an exclusive lock for a calendar or an event.
*
* @param string $calendar The id of the calendar to lock
* @param string $event The uid for the event to lock
*
* @return mixed A lock ID on success, false if:
* - The calendar is already locked
* - The event is already locked
* - A calendar lock was requested and an event is
* already locked in the calendar
* @throws Kronolith_Exception
*/
public function lock($calendar, $event = null)
{
if (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
if (!empty($event)) {
$uid = $calendar . ':' . $event;
}
return $GLOBALS['injector']->getInstance('Kronolith_Shares')->getShare($calendar)->lock($GLOBALS['injector']->getInstance('Horde_Lock'), $uid);
}
/**
* Releases a lock.
*
* @param array $calendar The event to lock.
* @param array $lockid The lock id to unlock.
*
* @throws Kronolith_Exception
*/
public function unlock($calendar, $lockid)
{
if (!Kronolith::hasPermission($calendar, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
return $GLOBALS['injector']->getInstance('Kronolith_Shares')->getShare($calendar)->unlock($GLOBALS['injector']->getInstance('Horde_Lock'), $lockid);
}
/**
* Check for existing calendar or event locks.
*
* @param array $calendar The calendar to check locks for.
* @param array $event The event to check locks for.
*
* @throws Kronolith_Exception
*/
public function checkLocks($calendar, $event = null)
{
if (!Kronolith::hasPermission($calendar, Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied();
}
if (!empty($event)) {
$uid = $calendar . ':' . $event;
}
return $GLOBALS['injector']->getInstance('Kronolith_Shares')->getShare($calendar)->checkLocks($GLOBALS['injector']->getInstance('Horde_Lock'), $uid);
}
/**
*
* @return array A list of calendars used to display free/busy information
*/
public function getFbCalendars()
{
return (unserialize($GLOBALS['prefs']->getValue('fb_cals')));
}
/**
* Retrieve the list of used tag_names, tag_ids and the total number
* of resources that are linked to that tag.
*
* @param array $tags An optional array of tag_ids. If omitted, all tags
* will be included.
* @param string $user Restrict result to those tagged by $user.
*
* @return array An array containing tag_name, and total
*/
public function listTagInfo($tags = null, $user = null)
{
return $GLOBALS['injector']
->getInstance('Kronolith_Tagger')->getTagInfo($tags, 500, null, $user);
}
/**
* SearchTags API:
* Returns an application-agnostic array (useful for when doing a tag search
* across multiple applications)
*
* The 'raw' results array can be returned instead by setting $raw = true.
*
* @param array $names An array of tag_names to search for.
* @param integer $max The maximum number of resources to return.
* @param integer $from The number of the resource to start with.
* @param string $resource_type The resource type [event, calendar, '']
* @param string $user Restrict results to resources owned by $user.
* @param boolean $raw Return the raw data?
*
* @return array An array of results:
*
* 'title' - The title for this resource.
* 'desc' - A terse description of this resource.
* 'view_url' - The URL to view this resource.
* 'app' - The Horde application this resource belongs to.
*
*/
public function searchTags($names, $max = 10, $from = 0,
$resource_type = '', $user = null, $raw = false)
{
// TODO: $max, $from, $resource_type not honored
$results = $GLOBALS['injector']
->getInstance('Kronolith_Tagger')
->search(
$names,
array('type' => 'event', 'user' => $user));
// Check for error or if we requested the raw data array.
if ($raw) {
return $results;
}
$return = array();
if (!empty($results['events'])) {
foreach ($results['events'] as $event_id) {
$driver = Kronolith::getDriver();
$event = $driver->getByUid($event_id);
$view_url = $event->getViewUrl();
$return[] = array(
'title' => $event->title,
'desc'=> $event->start->strftime($GLOBALS['prefs']->getValue('date_format_mini')) . ' ' . $event->start->strftime($GLOBALS['prefs']->getValue('time_format')),
'view_url' => $view_url,
'app' => 'kronolith'
);
}
}
return $return;
}
/**
* Create a new calendar for the existing user.
*
* @param string $name The calendar's display name.
* @param array $param Any additional parameters. May include:
* - color: (string) The color to associate with the calendar.
* DEFAULT: none (color will be randomly assigned).
* - description: (string) The calendar description.
* DEFAULT: none (empty description).
* - tags: (array) An array of tags to apply to the new calendar.
*
* - synchronize: (boolean) If true, add calendar to the list of
* calendars to syncronize.
* DEFAULT: false (do not add to the list of calendars).
* @return string The new calendar's UID.
* @since 4.2.0
*/
public function addCalendar($name, array $params = array())
{
global $prefs;
$info = array(
'name' => $name,
'color' => empty($params['color']) ? null : $params['color'],
'description' => empty($params['description']) ? null : $params['description'],
'tags' => empty($params['tags']) ? null : $params['tags']
);
$share = Kronolith::addShare($info);
if (!empty($params['synchronize'])) {
$sync = @unserialize($prefs->getValue('sync_calendars'));
$sync[] = $share->getName();
$prefs->setValue('sync_calendars', serialize($sync));
}
return $share->getName();
}
/**
* Delete the specified calendar.
*
* @param string $id The calendar id.
*/
public function deleteCalendar($id)
{
$calendar = $GLOBALS['injector']
->getInstance('Kronolith_Shares')
->getShare($calendar);
Kronolith::deleteShare($calendar);
}
/**
* Return an internal calendar.
*
* @todo Note: This returns a Kronolith_Calendar_Object object instead of a hash
* to be consistent with other application APIs. For H6 we need to normalize
* the APIs to always return non-objects and/or implement some mechanism to
* mark API methods as non-RPC safe.
*
* @param string $id The calendar uid (share name).
*
* @return Kronolith_Calendar The calendar object.
* @since 4.2.0
*/
public function getCalendar($id = null)
{
$driver = Kronolith::getDriver(null, $id);
return Kronolith::getCalendar($driver);
}
/**
* Update an internal calendar's information.
*
* @param string $id The calendar id.
* @param array $info An array of calendar information.
* @see self::addCalendar()
* @since 4.2.0
*/
public function updateCalendar($id, array $info)
{
$calendar = $this->getCalendar(null, $id);
// Prevent wiping tags if they were not passed.
if (!array_key_exists('tags', $info)) {
$info['tags'] = Kronolith::getTagger()->getTags($id, Kronolith_Tagger::TYPE_CALENDAR);
}
Kronolith::updateShare($calendar->share(), $info);
}
}