1596 lines
62 KiB
PHP
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);
|
|
}
|
|
|
|
}
|