Files
server/usr/share/psa-horde/kronolith/lib/Api.php
2026-01-07 20:52:11 +01:00

1596 lines
62 KiB
PHP

<?php
/**
* Kronolith external API interface.
*
* This file defines Kronolith's external API interface. Other applications
* can interact with Kronolith through this API.
*
* @package Kronolith
*/
class Kronolith_Api extends Horde_Registry_Api
{
/**
* Links.
*
* @var array
*/
protected $_links = array(
'show' => '%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:
* <pre>
* text/calendar
* text/x-vcalendar
* activesync
* </pre>
* @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:
* <pre>
* 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)
* </pre>
* @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:
* <pre>
* text/calendar (VCALENDAR 2.0. Recommended as
* this is specified in rfc2445)
* text/x-vcalendar (old VCALENDAR 1.0 format.
* Still in wide use)
* </pre>
*
* @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:
* <pre>
* '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.
* </pre>
*/
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);
}
}