mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 14:41:29 +01:00
* Calendar: new REST API to read, create, modify and delete events, see https://github.com/EGroupware/egroupware/blob/master/doc/REST-CalDAV-CardDAV/Calendar.md
This commit is contained in:
parent
f04b25089a
commit
b013f75eef
@ -33,16 +33,17 @@ class JsCalendar
|
|||||||
*
|
*
|
||||||
* @param int|array $event
|
* @param int|array $event
|
||||||
* @param bool|"pretty" $encode=true true: JSON encode, "pretty": JSON encode with pretty-print, false: return raw data e.g. from listing
|
* @param bool|"pretty" $encode=true true: JSON encode, "pretty": JSON encode with pretty-print, false: return raw data e.g. from listing
|
||||||
|
* @param ?array $exceptions=null
|
||||||
* @return string|array
|
* @return string|array
|
||||||
* @throws Api\Exception\NotFound
|
* @throws Api\Exception\NotFound
|
||||||
*/
|
*/
|
||||||
public static function getJsEvent($event, $encode=true)
|
public static function JsEvent($event, $encode=true, array $exceptions=[])
|
||||||
{
|
{
|
||||||
if (is_scalar($event) && !($event = self::getCalendar()->read($event)))
|
if (is_scalar($event) && !($event = self::getCalendar()->read($event)))
|
||||||
{
|
{
|
||||||
throw new Api\Exception\NotFound();
|
throw new Api\Exception\NotFound();
|
||||||
}
|
}
|
||||||
$data = array_filter([
|
$data = [
|
||||||
self::AT_TYPE => self::TYPE_EVENT,
|
self::AT_TYPE => self::TYPE_EVENT,
|
||||||
'prodId' => 'EGroupware Calendar '.$GLOBALS['egw_info']['apps']['api']['version'],
|
'prodId' => 'EGroupware Calendar '.$GLOBALS['egw_info']['apps']['api']['version'],
|
||||||
'uid' => self::uid($event['uid']),
|
'uid' => self::uid($event['uid']),
|
||||||
@ -54,16 +55,25 @@ class JsCalendar
|
|||||||
'timeZone' => $event['tzid'],
|
'timeZone' => $event['tzid'],
|
||||||
'showWithoutTime' => $event['whole_day'],
|
'showWithoutTime' => $event['whole_day'],
|
||||||
'duration' => self::Duration($event['start'], $event['end'], $event['whole_day']),
|
'duration' => self::Duration($event['start'], $event['end'], $event['whole_day']),
|
||||||
|
'recurrenceRules' => null,
|
||||||
|
'recurrenceOverrides' => null,
|
||||||
'freeBusyStatus' => $event['non_blocking'] ? 'free' : null, // default is busy
|
'freeBusyStatus' => $event['non_blocking'] ? 'free' : null, // default is busy
|
||||||
'description' => $event['description'],
|
'description' => $event['description'],
|
||||||
'participants' => self::Participants($event),
|
'participants' => self::Participants($event),
|
||||||
|
'alerts' => self::Alerts($event['alarm']),
|
||||||
'status' => empty($event['deleted']) ? 'confirmed' : 'cancelled', // we have no "tentative" event-status (only participants)!
|
'status' => empty($event['deleted']) ? 'confirmed' : 'cancelled', // we have no "tentative" event-status (only participants)!
|
||||||
'priority' => self::Priority($event['priority']),
|
'priority' => self::Priority($event['priority']),
|
||||||
'categories' => self::categories($event['category']),
|
'categories' => self::categories($event['category']),
|
||||||
'privacy' => $event['public'] ? 'public' : 'private',
|
'privacy' => $event['public'] ? 'public' : 'private',
|
||||||
'alerts' => self::Alerts($event['alarms']),
|
|
||||||
'egroupware.org:customfields' => self::customfields($event),
|
'egroupware.org:customfields' => self::customfields($event),
|
||||||
]+self::Locations($event)+self::Recurrence($event));
|
] + self::Locations($event);
|
||||||
|
|
||||||
|
if (!empty($event['recur_type']))
|
||||||
|
{
|
||||||
|
$data = array_merge($data, self::Recurrence($event, $data, $exceptions));
|
||||||
|
}
|
||||||
|
$data = array_filter($data);
|
||||||
|
|
||||||
if ($encode)
|
if ($encode)
|
||||||
{
|
{
|
||||||
return Api\CalDAV::json_encode($data, $encode === "pretty");
|
return Api\CalDAV::json_encode($data, $encode === "pretty");
|
||||||
@ -149,7 +159,13 @@ class JsCalendar
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'alerts':
|
case 'alerts':
|
||||||
$event['alarms'] = self::parseAlerts($value);
|
throw new \Exception('Creating or modifying alerts is NOT (yet) implemented!');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'recurrenceRules':
|
||||||
|
case 'recurrenceOverrides':
|
||||||
|
case 'excludedRecurrenceRules':
|
||||||
|
throw new \Exception('Creating or modifying recurring events is NOT (yet) implemented!');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'categories':
|
case 'categories':
|
||||||
@ -174,6 +190,13 @@ class JsCalendar
|
|||||||
catch (\Throwable $e) {
|
catch (\Throwable $e) {
|
||||||
self::handleExceptions($e, 'JsCalendar Event', $name, $value);
|
self::handleExceptions($e, 'JsCalendar Event', $name, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if no participant given add current user as CHAIR to the event
|
||||||
|
if (empty($event['participants']))
|
||||||
|
{
|
||||||
|
$event['participants'][$GLOBALS['egw_info']['user']['account_id']] = 'ACHAIR';
|
||||||
|
}
|
||||||
|
|
||||||
return $event;
|
return $event;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,13 +561,15 @@ class JsCalendar
|
|||||||
$info = self::getCalendar()->resource_info($uid) ?: [];
|
$info = self::getCalendar()->resource_info($uid) ?: [];
|
||||||
switch($info['type'] ?? $info['app'])
|
switch($info['type'] ?? $info['app'])
|
||||||
{
|
{
|
||||||
case 'user':
|
case 'e': // email
|
||||||
|
case 'c': // contact
|
||||||
|
case 'u': // user
|
||||||
$info['kind'] = 'individual';
|
$info['kind'] = 'individual';
|
||||||
break;
|
break;
|
||||||
case 'group':
|
case 'g':
|
||||||
$info['kind'] = 'group';
|
$info['kind'] = 'group';
|
||||||
break;
|
break;
|
||||||
case 'resources':
|
case 'r':
|
||||||
$info['kind'] = Api\CalDAV\Principals::resource_is_location($user_id) ? 'location' : 'resource';
|
$info['kind'] = Api\CalDAV\Principals::resource_is_location($user_id) ? 'location' : 'resource';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -552,7 +577,7 @@ class JsCalendar
|
|||||||
catch (\Exception $e) {
|
catch (\Exception $e) {
|
||||||
$info = [];
|
$info = [];
|
||||||
}
|
}
|
||||||
$participant = [
|
$participant = array_filter([
|
||||||
self::AT_TYPE => self::TYPE_PARTICIPANT,
|
self::AT_TYPE => self::TYPE_PARTICIPANT,
|
||||||
'name' => $info['name'] ?? null,
|
'name' => $info['name'] ?? null,
|
||||||
'email' => $info['email'] ?? null,
|
'email' => $info['email'] ?? null,
|
||||||
@ -565,7 +590,7 @@ class JsCalendar
|
|||||||
'informational' => $role === 'NON-PARTICIPANT',
|
'informational' => $role === 'NON-PARTICIPANT',
|
||||||
]),
|
]),
|
||||||
'participationStatus' => $status2jscal[$status],
|
'participationStatus' => $status2jscal[$status],
|
||||||
];
|
]);
|
||||||
$participants[$uid] = $participant;
|
$participants[$uid] = $participant;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,22 +637,122 @@ class JsCalendar
|
|||||||
return $priority_egw2jscal[$priority];
|
return $priority_egw2jscal[$priority];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TYPE_RECURRENCE_RULE = 'RecurrenceRule';
|
||||||
|
const TYPE_NDAY = 'NDay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return recurrence properties: recurrenceId, recurrenceRules, recurrenceOverrides, ...
|
* Return recurrence properties: recurrenceId, recurrenceRules, recurrenceOverrides, ...
|
||||||
*
|
*
|
||||||
* @TODO
|
* EGroupware only supports a subset of iCal recurrence rules (e.g. only byDay and byMonthDay, no other by-types)!
|
||||||
|
*
|
||||||
* @param array $event
|
* @param array $event
|
||||||
|
* @param array $data JSCalendar representation of event to calculate overrides
|
||||||
|
* @param array $exceptions exceptions
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected static function Recurrence(array $event)
|
protected static function Recurrence(array $event, array $data, array $exceptions=[])
|
||||||
{
|
{
|
||||||
return [];
|
if (empty($event['recur_type']))
|
||||||
|
{
|
||||||
|
return []; // non-recurring event
|
||||||
|
}
|
||||||
|
$rriter = \calendar_rrule::event2rrule($event, false);
|
||||||
|
$rrule = $rriter->generate_rrule('2.0');
|
||||||
|
$rule = array_filter([
|
||||||
|
self::AT_TYPE => self::TYPE_RECURRENCE_RULE,
|
||||||
|
'frequency' => strtolower($rrule['FREQ']),
|
||||||
|
'interval' => $rrule['INTERVAL'] ?? null,
|
||||||
|
'until' => empty($rrule['UNTIL']) ? null : self::DateTime($rrule['UNTIL'], $event['tzid']),
|
||||||
|
]);
|
||||||
|
if (!empty($GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts']) &&
|
||||||
|
$GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts'] !== 'Monday')
|
||||||
|
{
|
||||||
|
$rule['firstDayOfWeek'] = strtolower(substr($GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts'], 0, 2));
|
||||||
|
}
|
||||||
|
if (!empty($rrule['BYDAY']))
|
||||||
|
{
|
||||||
|
$rule['byDay'] = array_filter([
|
||||||
|
self::AT_TYPE => self::TYPE_NDAY,
|
||||||
|
'day' => strtolower(substr($rrule['BYDAY'], $rriter->monthly_byday_num ? strlen((string)$rriter->monthly_byday_num) : 0)),
|
||||||
|
'nthOfPeriod' => $rriter->monthly_byday_num,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
elseif (!empty($rrule['BYMONTHDAY']))
|
||||||
|
{
|
||||||
|
$rule['byMonthDay'] = [$rrule['BYMONTHDAY']]; // EGroupware supports only a single day!
|
||||||
|
}
|
||||||
|
|
||||||
|
$overrides = [];
|
||||||
|
// adding excludes to the overrides
|
||||||
|
if (!empty($event['recur_exception']))
|
||||||
|
{
|
||||||
|
foreach ($event['recur_exception'] as $timestamp)
|
||||||
|
{
|
||||||
|
$ex_date = new Api\DateTime($timestamp, Api\DateTime::$server_timezone);
|
||||||
|
if (!empty($event['whole_day']))
|
||||||
|
{
|
||||||
|
$ex_date->setTime(0, 0, 0);
|
||||||
|
}
|
||||||
|
$overrides[self::DateTime($ex_date, $event['tzid'])] = [
|
||||||
|
'excluded' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// adding exceptions to the overrides
|
||||||
|
foreach($exceptions as $exception)
|
||||||
|
{
|
||||||
|
$overrides[self::DateTime($exception['recurrence'], $event['tzid'])] = self::getPatch(self::JsEvent($exception, false), $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter([
|
||||||
|
'recurrenceRules' => [$rule],
|
||||||
|
'recurrenceOverrides' => $overrides,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get patch from an event / recurrence compared to the master event
|
||||||
|
*
|
||||||
|
* @param array $event
|
||||||
|
* @param array $master
|
||||||
|
* @return array with modified attributes
|
||||||
|
*/
|
||||||
|
public static function getPatch(array $event, array $master=null)
|
||||||
|
{
|
||||||
|
if (!$master)
|
||||||
|
{
|
||||||
|
return $event;
|
||||||
|
}
|
||||||
|
// array_diff_assoc only reports values changed or set in $event for scalar values
|
||||||
|
$patch = array_diff_assoc($event, $master);
|
||||||
|
|
||||||
|
// we need to report unset / removed values
|
||||||
|
foreach($master as $name => $value)
|
||||||
|
{
|
||||||
|
if (isset($value) && !isset($event[$name]))
|
||||||
|
{
|
||||||
|
$patch[$name] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for non-scalar values, we have to call ourselves recursive
|
||||||
|
foreach($event as $name => $value)
|
||||||
|
{
|
||||||
|
if (is_array($value) && ($diff = self::getPatch($event[$name], $master[$name])))
|
||||||
|
{
|
||||||
|
$patch[$name] = $diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_ALERT = 'Alert';
|
||||||
|
const TYPE_OFFSET_TRIGGER = 'OffsetTrigger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return alerts object
|
* Return alerts object
|
||||||
*
|
*
|
||||||
* @TODO
|
|
||||||
* @param array|null $alarms
|
* @param array|null $alarms
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
@ -636,7 +761,19 @@ class JsCalendar
|
|||||||
$alerts = [];
|
$alerts = [];
|
||||||
foreach($alarms ?? [] as $alarm)
|
foreach($alarms ?? [] as $alarm)
|
||||||
{
|
{
|
||||||
|
if (!isset($alarm['offset']) || empty($alarm['all']) && $alarm['owner'] != $GLOBALS['egw_info']['user']['account_id'])
|
||||||
|
{
|
||||||
|
continue; // do NOT show other users alarms
|
||||||
|
}
|
||||||
|
$alerts[$alarm['uid']] = array_filter([
|
||||||
|
self::AT_TYPE => self::TYPE_ALERT,
|
||||||
|
'trigger' => [
|
||||||
|
self::AT_TYPE => self::TYPE_OFFSET_TRIGGER,
|
||||||
|
'offset' => $alarm['offset'],
|
||||||
|
],
|
||||||
|
'acknowledged' => empty($alarm['attrs']['ACKNOWLEDGED']['value']) ? null :
|
||||||
|
self::UTCDateTime(new Api\DateTime($alarm['attrs']['ACKNOWLEDGED']['value'])),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return $alerts;
|
return $alerts;
|
||||||
}
|
}
|
||||||
|
@ -1128,7 +1128,7 @@ class calendar_bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts all repetions of $event in the timespan between $start and $end into $events
|
* Inserts all repetitions of $event in the timespan between $start and $end into $events
|
||||||
*
|
*
|
||||||
* The new entries are just appended to $events, so $events is no longer sorted by startdate !!!
|
* The new entries are just appended to $events, so $events is no longer sorted by startdate !!!
|
||||||
*
|
*
|
||||||
@ -2191,9 +2191,9 @@ class calendar_bo
|
|||||||
$entry = $this->read($id, $recur_date, true, 'server');
|
$entry = $this->read($id, $recur_date, true, 'server');
|
||||||
}
|
}
|
||||||
$etag = $schedule_tag = $entry['id'].':'.$entry['etag'];
|
$etag = $schedule_tag = $entry['id'].':'.$entry['etag'];
|
||||||
$etag .= ':'.$entry['modified'];
|
$etag .= ':'.Api\DateTime::user2server($entry['modified'], 'ts');
|
||||||
|
|
||||||
//error_log(__METHOD__ . "($entry[id],$client_share_uid_excpetions) entry=".array2string($entry)." --> etag=$etag");
|
//error_log(__METHOD__ . "($entry[id],$client_share_uid_exceptions) entry=".array2string($entry)." --> etag=$etag");
|
||||||
return $etag;
|
return $etag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,10 +383,11 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
$events = null;
|
$events = null;
|
||||||
$is_jscalendar = Api\CalDAV::isJSON();
|
$is_jscalendar = Api\CalDAV::isJSON();
|
||||||
for($chunk=0; (!$chunk || count($events) === self::CHUNK_SIZE) && // stop after we have not got a full chunk
|
for($chunk=0; (!$chunk || count($events) === self::CHUNK_SIZE) && // stop after we have not got a full chunk
|
||||||
($events =& $this->bo->search($filter+[
|
($events =& $this->bo->search([
|
||||||
'offset' => $chunk*self::CHUNK_SIZE,
|
'offset' => $chunk*self::CHUNK_SIZE,
|
||||||
'num_rows' => self::CHUNK_SIZE,
|
'num_rows' => self::CHUNK_SIZE,
|
||||||
])); ++$chunk)
|
'date_format' => $is_jscalendar ? 'object' : 'ts',
|
||||||
|
]+$filter)); ++$chunk)
|
||||||
{
|
{
|
||||||
foreach($events as $event)
|
foreach($events as $event)
|
||||||
{
|
{
|
||||||
@ -441,11 +442,10 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
//error_log(__FILE__ . __METHOD__ . "Calendar Data : $calendar_data");
|
//error_log(__FILE__ . __METHOD__ . "Calendar Data : $calendar_data");
|
||||||
if ($calendar_data)
|
if ($calendar_data)
|
||||||
{
|
{
|
||||||
$content = $is_jscalendar ? Api\CalDAV\JsCalendar::getJsEvent($event, false) :
|
$content = $this->iCal($event, $filter['users'],
|
||||||
$this->iCal($event, $filter['users'],
|
|
||||||
strpos($path, '/inbox/') !== false ? 'REQUEST' : null,
|
strpos($path, '/inbox/') !== false ? 'REQUEST' : null,
|
||||||
!isset($calendar_data['children']['expand']) ? false :
|
!isset($calendar_data['children']['expand']) ? false :
|
||||||
($calendar_data['children']['expand']['attrs'] ?: true), $exceptions);
|
($calendar_data['children']['expand']['attrs'] ?: true), $exceptions, $is_jscalendar ? false : null);
|
||||||
$props['getcontentlength'] = bytes($is_jscalendar ? json_encode($content) : $content);
|
$props['getcontentlength'] = bytes($is_jscalendar ? json_encode($content) : $content);
|
||||||
$props['calendar-data'] = Api\CalDAV::mkprop(Api\CalDAV::CALDAV,'calendar-data',$content);
|
$props['calendar-data'] = Api\CalDAV::mkprop(Api\CalDAV::CALDAV,'calendar-data',$content);
|
||||||
}
|
}
|
||||||
@ -578,10 +578,12 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
}
|
}
|
||||||
if ($time)
|
if ($time)
|
||||||
{
|
{
|
||||||
$props['dtstamp'] = Api\CalDAV::mkprop(Api\CalDAV::CALENDARSERVER, 'dtstamp', gmdate('Ymd\\This\\Z', $time));
|
$dtstamp = Api\DateTime::to($time, 'object');
|
||||||
|
$dtstamp->setTimezone(new DateTimeZone('utc'));
|
||||||
|
$props['dtstamp'] = Api\CalDAV::mkprop(Api\CalDAV::CALENDARSERVER, 'dtstamp', $dtstamp->format('Ymd\\This\\Z'));
|
||||||
}
|
}
|
||||||
//error_log(__METHOD__."($user, $time) returning ".array2string($props));
|
//error_log(__METHOD__."($user, $time) returning ".array2string($props));
|
||||||
return $props ? $props : '';
|
return $props ?: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -763,12 +765,12 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
// jsEvent or iCal
|
// jsEvent or iCal
|
||||||
if (($type=Api\CalDAV::isJSON()))
|
if (($type=Api\CalDAV::isJSON()))
|
||||||
{
|
{
|
||||||
$options['data'] = Api\CalDAV\JsCalendar::getJsEvent($event, $type);
|
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null, false, null, $type);
|
||||||
$options['mimetype'] = Api\CalDAV\JsCalendar::MIME_TYPE_JSEVENT.';charset=utf-8';
|
$options['mimetype'] = Api\CalDAV\JsCalendar::MIME_TYPE_JSEVENT.';charset=utf-8';
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null);
|
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null, false, null);
|
||||||
$options['mimetype'] = 'text/calendar; charset=utf-8';
|
$options['mimetype'] = 'text/calendar; charset=utf-8';
|
||||||
}
|
}
|
||||||
header('Content-Encoding: identity');
|
header('Content-Encoding: identity');
|
||||||
@ -784,16 +786,17 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
/**
|
/**
|
||||||
* Generate an iCal for the given event
|
* Generate an iCal for the given event
|
||||||
*
|
*
|
||||||
* Taking into account virtual an real exceptions for recuring events
|
* Taking into account virtual and real exceptions for recurring events
|
||||||
*
|
*
|
||||||
* @param array $event
|
* @param array $event
|
||||||
* @param int $user =null account_id of calendar to display
|
* @param int $user =null account_id of calendar to display
|
||||||
* @param string $method =null eg. 'PUBLISH' for inbox, nothing anywhere else
|
* @param string $method =null e.g. 'PUBLISH' for inbox, nothing anywhere else
|
||||||
* @param boolean|array $expand =false true or array with values for 'start', 'end' to expand recurrences
|
* @param boolean|array $expand =false true or array with values for 'start', 'end' to expand recurrences
|
||||||
* @param array|null $events as returned by get_series() for !$expand (to not read them again)
|
* @param array|null $events as returned by get_series() for !$expand (to not read them again)
|
||||||
* @return string
|
* @param bool|"pretty"|null $json null: iCal, false: return array with JSCalendar data, true+"pretty": return json-serialized JSCalendar
|
||||||
|
* @return string|array array if $json === false
|
||||||
*/
|
*/
|
||||||
private function iCal(array $event,$user=null, $method=null, $expand=false, array $events=null)
|
private function iCal(array $event,$user=null, $method=null, $expand=false, array $events=null, $json=null)
|
||||||
{
|
{
|
||||||
static $handler = null;
|
static $handler = null;
|
||||||
if (is_null($handler)) $handler = $this->_get_handler();
|
if (is_null($handler)) $handler = $this->_get_handler();
|
||||||
@ -843,7 +846,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
// pass in original event as master, as it has correct start-date even if first recurrence is an exception
|
// pass in original event as master, as it has correct start-date even if first recurrence is an exception
|
||||||
if ($expand || !isset($events))
|
if ($expand || !isset($events))
|
||||||
{
|
{
|
||||||
$events =& self::get_series($event['uid'], $this->bo, $expand, $user, $event);
|
$events =& self::get_series($event['uid'], $this->bo, $expand, $user, $event, isset($json) ? 'object' : 'server');
|
||||||
}
|
}
|
||||||
// as alarm is now only on next recurrence, set alarm from original event on master
|
// as alarm is now only on next recurrence, set alarm from original event on master
|
||||||
if ($event['alarm']) $events[0]['alarm'] = $event['alarm'];
|
if ($event['alarm']) $events[0]['alarm'] = $event['alarm'];
|
||||||
@ -857,6 +860,10 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
$events[0]['uid'] .= '-'.$event['id']; // force a different uid
|
$events[0]['uid'] .= '-'.$event['id']; // force a different uid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isset($json))
|
||||||
|
{
|
||||||
|
return Api\CalDAV\JsCalendar::JsEvent($events[0], $json, array_slice($events, 1));
|
||||||
|
}
|
||||||
return $handler->exportVCal($events, '2.0', $method);
|
return $handler->exportVCal($events, '2.0', $method);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -872,7 +879,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
* @param array $master =null use provided event as master to fix wrong start-date if first recurrence is an exception
|
* @param array $master =null use provided event as master to fix wrong start-date if first recurrence is an exception
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private static function &get_series($uid,calendar_bo $bo=null, $expand=false, $user=null, $master=null)
|
private static function &get_series($uid,calendar_bo $bo=null, $expand=false, $user=null, $master=null, $date_format='server')
|
||||||
{
|
{
|
||||||
if (is_null($bo)) $bo = new calendar_boupdate();
|
if (is_null($bo)) $bo = new calendar_boupdate();
|
||||||
|
|
||||||
@ -880,7 +887,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
'query' => array('cal_uid' => $uid),
|
'query' => array('cal_uid' => $uid),
|
||||||
'filter' => 'owner', // return all possible entries
|
'filter' => 'owner', // return all possible entries
|
||||||
'daywise' => false,
|
'daywise' => false,
|
||||||
'date_format' => 'server',
|
'date_format' => $date_format,
|
||||||
'cfs' => array(), // read cfs as we use them to store X- attributes
|
'cfs' => array(), // read cfs as we use them to store X- attributes
|
||||||
);
|
);
|
||||||
if (is_array($expand)) $params += $expand;
|
if (is_array($expand)) $params += $expand;
|
||||||
@ -891,7 +898,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
return $events;
|
return $events;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find master, which is not always first event, eg. when first event is an exception
|
// find master, which is not always first event, e.g. when first event is an exception
|
||||||
$exceptions = array();
|
$exceptions = array();
|
||||||
foreach($events as $k => &$recurrence)
|
foreach($events as $k => &$recurrence)
|
||||||
{
|
{
|
||||||
@ -904,7 +911,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if recurring event starts in future behind horizont, nothing will be returned by bo::search()
|
// if recurring event starts in future behind horizont, nothing will be returned by bo::search()
|
||||||
if (!isset($master)) $master = $bo->read($uid);
|
if (!isset($master)) $master = $bo->read($uid, null, false, $date_format);
|
||||||
|
|
||||||
foreach($events as $k => &$recurrence)
|
foreach($events as $k => &$recurrence)
|
||||||
{
|
{
|
||||||
@ -912,7 +919,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
if ($master && $recurrence['reference'] && $recurrence['reference'] != $master['id'])
|
if ($master && $recurrence['reference'] && $recurrence['reference'] != $master['id'])
|
||||||
{
|
{
|
||||||
unset($events[$k]);
|
unset($events[$k]);
|
||||||
continue; // same uid, but references a different event or is own master
|
continue; // same uid, but references a different event or its own master
|
||||||
}
|
}
|
||||||
if (!$master || $recurrence['id'] != $master['id']) // real exception
|
if (!$master || $recurrence['id'] != $master['id']) // real exception
|
||||||
{
|
{
|
||||||
@ -929,7 +936,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
$recurrence['participants'][$user] = 'R';
|
$recurrence['participants'][$user] = 'R';
|
||||||
}
|
}
|
||||||
//error_log('real exception: '.array2string($recurrence));
|
//error_log('real exception: '.array2string($recurrence));
|
||||||
// remove from masters recur_exception, as exception is include
|
// remove from masters recur_exception, as exception is included
|
||||||
// at least Lightning "understands" EXDATE as exception from what's included
|
// at least Lightning "understands" EXDATE as exception from what's included
|
||||||
// in the whole resource / VCALENDAR component
|
// in the whole resource / VCALENDAR component
|
||||||
// not removing it causes Lightning to remove the exception itself
|
// not removing it causes Lightning to remove the exception itself
|
||||||
@ -1620,12 +1627,13 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||||||
{
|
{
|
||||||
if (strpos($column=self::$path_attr,'_') === false) $column = 'cal_'.$column;
|
if (strpos($column=self::$path_attr,'_') === false) $column = 'cal_'.$column;
|
||||||
|
|
||||||
$event = $this->bo->read(array($column => $id, 'cal_deleted IS NULL', 'cal_reference=0'), null, true, 'server');
|
$event = $this->bo->read(array($column => $id, 'cal_deleted IS NULL', 'cal_reference=0'), null, true,
|
||||||
|
$date_format = Api\CalDAV::isJSON() ? 'object' : 'server');
|
||||||
if ($event) $event = array_shift($event); // read with array as 1. param, returns an array of events!
|
if ($event) $event = array_shift($event); // read with array as 1. param, returns an array of events!
|
||||||
|
|
||||||
if (!($retval = $this->bo->check_perms(calendar_bo::ACL_FREEBUSY,$event, 0, 'server')) &&
|
if (!($retval = $this->bo->check_perms(calendar_bo::ACL_FREEBUSY,$event, 0, 'server')) &&
|
||||||
// above can be true, if current user is not in master but just a recurrence
|
// above can be true, if current user is not in master but just a recurrence
|
||||||
(!$event['recur_type'] || !($events = self::get_series($event['uid'], $this->bo))))
|
(!$event['recur_type'] || !($events = self::get_series($event['uid'], $this->bo, false, null, null, $date_format))))
|
||||||
{
|
{
|
||||||
if ($this->debug > 0) error_log(__METHOD__."($id) no READ or FREEBUSY rights returning ".array2string($retval));
|
if ($this->debug > 0) error_log(__METHOD__."($id) no READ or FREEBUSY rights returning ".array2string($retval));
|
||||||
return $retval;
|
return $retval;
|
||||||
|
@ -538,9 +538,9 @@ class calendar_rrule implements Iterator
|
|||||||
$previous = clone $this->current;
|
$previous = clone $this->current;
|
||||||
$this->next_no_exception();
|
$this->next_no_exception();
|
||||||
}
|
}
|
||||||
// if enddate is now before next acurrence, but not on same day, we use previous recurrence
|
// if enddate is now before next occurrence, but not on same day, we use previous recurrence
|
||||||
// this can happen if client gives an enddate which is NOT a recurrence date
|
// this can happen if client gives an enddate which is NOT a recurrence date
|
||||||
// eg. for a on Monday recurring weekly event a Tuesday as enddate
|
// e.g. for an on Monday recurring weekly event a Tuesday as enddate
|
||||||
if ($this->enddate < $this->current && $this->current->format('Ymd') != $this->enddate->format('Ymd'))
|
if ($this->enddate < $this->current && $this->current->format('Ymd') != $this->enddate->format('Ymd'))
|
||||||
{
|
{
|
||||||
$last = $previous;
|
$last = $previous;
|
||||||
|
504
doc/REST-CalDAV-CardDAV/Addressbook.md
Normal file
504
doc/REST-CalDAV-CardDAV/Addressbook.md
Normal file
@ -0,0 +1,504 @@
|
|||||||
|
# EGroupware REST API for Addressbook
|
||||||
|
|
||||||
|
Authentication is via Basic Auth with username and a password, or a token valid for:
|
||||||
|
- either just the given user or all users
|
||||||
|
- CalDAV/CardDAV Sync (REST API)
|
||||||
|
- Calendar application
|
||||||
|
|
||||||
|
Following RFCs / drafts used/planned for JSON encoding of resources
|
||||||
|
* [draft-ietf-jmap-jscontact: JSContact: A JSON Representation of Contact Data](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact)
|
||||||
|
([* see at end of document](#implemented-changes-from-jscontact-draft-08))
|
||||||
|
* [draft-ietf-jmap-jscontact-vcard: JSContact: Converting from and to vCard](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-vcard/)
|
||||||
|
* [rfc8984: JSCalendar: A JSON Representation of Calendar Data](https://datatracker.ietf.org/doc/html/rfc8984)
|
||||||
|
|
||||||
|
### Supported request methods and examples
|
||||||
|
|
||||||
|
* **GET** to collections with an ```Accept: application/json``` header return all resources (similar to WebDAV PROPFIND)
|
||||||
|
<details>
|
||||||
|
<summary>Example: Getting all entries of a given users addessbook</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Accept: application/pretty+json" --user <username>
|
||||||
|
{
|
||||||
|
"responses": {
|
||||||
|
"/<username>/addressbook/1833": {
|
||||||
|
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||||
|
"prodId": "EGroupware Addressbook 21.1.001",
|
||||||
|
"created": "2010-10-21T09:55:42Z",
|
||||||
|
"updated": "2014-06-02T14:45:24Z",
|
||||||
|
"name": [
|
||||||
|
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
|
||||||
|
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
|
||||||
|
],
|
||||||
|
"fullName": { "value": "Default Tester" },
|
||||||
|
"organizations": {
|
||||||
|
"org": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "default.org",
|
||||||
|
"units": {
|
||||||
|
"org_unit": "department.default.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emails": {
|
||||||
|
"work": { "@type": "EmailAddress", "email": "test@test.com", "contexts": { "work": true }, "pref": 1 }
|
||||||
|
},
|
||||||
|
"phones": {
|
||||||
|
"tel_work": { "@type": "Phone", "phone": "+49 123 4567890", "pref": 1, "features": { "voice": true }, "contexts": { "work": true } },
|
||||||
|
"tel_cell": { "@type": "Phone", "phone": "012 3723567", "features": { "cell": true }, "contexts": { "work": true } }
|
||||||
|
},
|
||||||
|
"online": {
|
||||||
|
"url": { "@type": "Resource", "resource": "https://www.test.com/", "type": "uri", "contexts": { "work": true } }
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"Test test TEST\n\\server\\share\n\\\nother\nblah"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"/<username>/addressbook/list-36": {
|
||||||
|
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
|
||||||
|
"name": "Example distribution list",
|
||||||
|
"card": {
|
||||||
|
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
|
||||||
|
"prodId": "EGroupware Addressbook 21.1.001",
|
||||||
|
"updated": "2018-04-11T14:46:43Z",
|
||||||
|
"fullName": { "value": "Example distribution list" }
|
||||||
|
},
|
||||||
|
"members": {
|
||||||
|
"5638-8623c4830472a8ede9f9f8b30d435ea4": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
following GET parameters are supported to customize the returned properties:
|
||||||
|
- props[]=<DAV-prop-name> eg. props[]=getetag to return only the ETAG (multiple DAV properties can be specified)
|
||||||
|
Default for addressbook collections is to only return address-data (JsContact), other collections return all props.
|
||||||
|
- sync-token=<token> to only request change since last sync-token, like rfc6578 sync-collection REPORT
|
||||||
|
- nresults=N limit number of responses (only for sync-collection / given sync-token parameter!)
|
||||||
|
this will return a "more-results"=true attribute and a new "sync-token" attribute to query for the next chunk
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: Getting just ETAGs and displayname of all contacts in a given AB</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/?props[]=getetag&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
||||||
|
{
|
||||||
|
"responses": {
|
||||||
|
"/addressbook/1833": {
|
||||||
|
"displayname": "Default Tester",
|
||||||
|
"getetag": "\"1833:24\""
|
||||||
|
},
|
||||||
|
"/addressbook/1838": {
|
||||||
|
"displayname": "Test Tester",
|
||||||
|
"getetag": "\"1838:19\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: Start using a sync-token to get only changed entries since last sync</summary>
|
||||||
|
|
||||||
|
#### Initial request with empty sync-token and only requesting 10 entries per chunk:
|
||||||
|
```
|
||||||
|
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
||||||
|
{
|
||||||
|
"responses": {
|
||||||
|
"/addressbook/2050": "Frau Margot Test-Notifikation",
|
||||||
|
"/addressbook/2384": "Test Tester",
|
||||||
|
"/addressbook/5462": "Margot Testgedöns",
|
||||||
|
"/addressbook/2380": "Frau Test Defaulterin",
|
||||||
|
"/addressbook/5474": "Noch ein Neuer",
|
||||||
|
"/addressbook/5575": "Mr New Name",
|
||||||
|
"/addressbook/5461": "Herr Hugo Kurt Müller Senior",
|
||||||
|
"/addressbook/5601": "Steve Jobs",
|
||||||
|
"/addressbook/5603": "Ralf Becker",
|
||||||
|
"/addressbook/1838": "Test Tester"
|
||||||
|
},
|
||||||
|
"more-results": true,
|
||||||
|
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1400867824"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
#### Requesting next chunk:
|
||||||
|
```
|
||||||
|
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
||||||
|
{
|
||||||
|
"responses": {
|
||||||
|
"/addressbook/1833": "Default Tester",
|
||||||
|
"/addressbook/5597": "Neuer Testschnuffi",
|
||||||
|
"/addressbook/5593": "Muster Max",
|
||||||
|
"/addressbook/5628": "2. Test Contact",
|
||||||
|
"/addressbook/5629": "Testen Tester",
|
||||||
|
"/addressbook/5630": "Testen Tester",
|
||||||
|
"/addressbook/5633": "Testen Tester",
|
||||||
|
"/addressbook/5635": "Test4 Tester",
|
||||||
|
"/addressbook/5638": "Test Kontakt",
|
||||||
|
"/addressbook/5636": "Test Default"
|
||||||
|
},
|
||||||
|
"more-results": true,
|
||||||
|
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1427103057"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: Requesting only changes since last sync</summary>
|
||||||
|
|
||||||
|
#### ```sync-token``` from last sync need to be specified (note the null for a deleted resource!)
|
||||||
|
```
|
||||||
|
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824' -H "Accept: application/pretty+json" --user <username>
|
||||||
|
{
|
||||||
|
"responses": {
|
||||||
|
"/addressbook/5597": null,
|
||||||
|
"/addressbook/5593": {
|
||||||
|
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||||
|
"prodId": "EGroupware Addressbook 21.1.001",
|
||||||
|
"created": "2010-10-21T09:55:42Z",
|
||||||
|
"updated": "2014-06-02T14:45:24Z",
|
||||||
|
"name": [
|
||||||
|
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
|
||||||
|
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
|
||||||
|
],
|
||||||
|
"fullName": "Default Tester",
|
||||||
|
....
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1427103057"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsContact or JsCalendar schema
|
||||||
|
<details>
|
||||||
|
<summary>Example: GET request for a single resource showcasing available fieldes</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
curl 'https://example.org/egroupware/groupdav.php/addressbook/6502' -H "Accept: application/pretty+json" --user <username>
|
||||||
|
{
|
||||||
|
"uid": "addressbook-6502-8623c4830472a8ede9f9f8b30d435ea4",
|
||||||
|
"prodId": "EGroupware Addressbook 21.1.003",
|
||||||
|
"created": "2022-12-14T13:35:02Z",
|
||||||
|
"updated": "2022-12-14T13:39:14Z",
|
||||||
|
"kind": "individual",
|
||||||
|
"name": [
|
||||||
|
{ "@type": "NameComponent", "type": "prefix", "value": "Prefix/Title" },
|
||||||
|
{ "@type": "NameComponent", "type": "personal", "value": "Frist" },
|
||||||
|
{ "@type": "NameComponent", "type": "additional", "value": "Middle" },
|
||||||
|
{ "@type": "NameComponent", "type": "surname", "value": "Last" },
|
||||||
|
{ "@type": "NameComponent", "type": "suffix", "value": "Postfix" }
|
||||||
|
],
|
||||||
|
"fullName": "Prefix/Title Frist Middle Last Postfix",
|
||||||
|
"organizations": {
|
||||||
|
"org": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Organisation",
|
||||||
|
"units": { "org_unit": "Department" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"title": {
|
||||||
|
"@type": "Title",
|
||||||
|
"title": "Postion",
|
||||||
|
"organization": "org"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"@type": "Title",
|
||||||
|
"title": "Occupation",
|
||||||
|
"organization": "org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emails": {
|
||||||
|
"work": {
|
||||||
|
"@type": "EmailAddress",
|
||||||
|
"email": "email@example.org",
|
||||||
|
"contexts": { "work": true },
|
||||||
|
"pref": 1
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"@type": "EmailAddress",
|
||||||
|
"email": "private.email@example.org",
|
||||||
|
"contexts": { "private": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phones": {
|
||||||
|
"tel_work": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "voice": true },
|
||||||
|
"contexts": { "work": true }
|
||||||
|
},
|
||||||
|
"tel_cell": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "cell": true },
|
||||||
|
"contexts": { "work": true }
|
||||||
|
},
|
||||||
|
"tel_fax": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "fax": true },
|
||||||
|
"contexts": { "work": true }
|
||||||
|
},
|
||||||
|
"tel_assistent": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "voice": true },
|
||||||
|
"contexts": { "assistant": true }
|
||||||
|
},
|
||||||
|
"tel_car": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "voice": true },
|
||||||
|
"contexts": { "car": true }
|
||||||
|
},
|
||||||
|
"tel_pager": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "pager": true },
|
||||||
|
"contexts": { "work": true }
|
||||||
|
},
|
||||||
|
"tel_home": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "voice": true },
|
||||||
|
"contexts": { "private": true }
|
||||||
|
},
|
||||||
|
"tel_fax_home": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "fax": true },
|
||||||
|
"contexts": { "private": true }
|
||||||
|
},
|
||||||
|
"tel_cell_private": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "cell": true },
|
||||||
|
"contexts": { "private": true }
|
||||||
|
},
|
||||||
|
"tel_other": {
|
||||||
|
"@type": "Phone",
|
||||||
|
"phone": "+1(234)5678901",
|
||||||
|
"features": { "voice": true },
|
||||||
|
"contexts": { "work": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"online": {
|
||||||
|
"url": {
|
||||||
|
"@type": "Resource",
|
||||||
|
"resource": "https://example.org",
|
||||||
|
"type": "uri",
|
||||||
|
"contexts": { "work": true }
|
||||||
|
},
|
||||||
|
"url_home": {
|
||||||
|
"@type": "Resource",
|
||||||
|
"resource": "https://private.example.org",
|
||||||
|
"type": "uri",
|
||||||
|
"contexts": { "private": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"addresses": {
|
||||||
|
"work": {
|
||||||
|
"@type": "Address",
|
||||||
|
"locality": "City",
|
||||||
|
"region": "Rheinland-Pfalz",
|
||||||
|
"country": "DEUTSCHLAND",
|
||||||
|
"postcode": "12345",
|
||||||
|
"countryCode": "DE",
|
||||||
|
"street": [
|
||||||
|
{ "@type": "StreetComponent", "type": "name", "value": "Street" },
|
||||||
|
{ "@type": "StreetComponent", "type": "separator", "value": "\n" },
|
||||||
|
{ "@type": "StreetComponent", "type": "name", "value": "Street2" ],
|
||||||
|
"contexts": { "work": true },
|
||||||
|
"pref": 1
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"@type": "Address",
|
||||||
|
"locality": "PrivateCity",
|
||||||
|
"country": "DEUTSCHLAND",
|
||||||
|
"postcode": "12345",
|
||||||
|
"countryCode": "DE",
|
||||||
|
"street": [
|
||||||
|
{ "@type": "StreetComponent", "type": "name", "value": "PrivateStreet" },
|
||||||
|
{ "@type": "StreetComponent", "type": "separator", "value": "\n" },
|
||||||
|
{ "@type": "StreetComponent", "type": "name", "value": "PrivateStreet2" }
|
||||||
|
],
|
||||||
|
"contexts": { "home": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"photos": {
|
||||||
|
"photo": {
|
||||||
|
"@type": "File",
|
||||||
|
"href": "https://boulder.egroupware.org/egroupware/api/avatar.php?contact_id=6502&lavatar=1&etag=0",
|
||||||
|
"mediaType": "image/jpeg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"anniversaries": {
|
||||||
|
"bday": {
|
||||||
|
"@type": "Anniversary",
|
||||||
|
"type": "birth",
|
||||||
|
"date": "2022-12-14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"Kategorie": true,
|
||||||
|
"My Contacts": true
|
||||||
|
},
|
||||||
|
"egroupware.org:assistant": "Assistent"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
* **POST** requests to collection with a ```Content-Type: application/json``` header add new entries in addressbook or calendar collections
|
||||||
|
(Location header in response gives URL of new resource)
|
||||||
|
<details>
|
||||||
|
<summary>Example: POST request to create a new resource</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
|
||||||
|
{
|
||||||
|
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||||
|
"prodId": "EGroupware Addressbook 21.1.001",
|
||||||
|
"created": "2010-10-21T09:55:42Z",
|
||||||
|
"updated": "2014-06-02T14:45:24Z",
|
||||||
|
"name": [
|
||||||
|
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
|
||||||
|
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
|
||||||
|
],
|
||||||
|
"fullName": { "value": "Default Tester" },
|
||||||
|
....
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: POST request to create a new resource using flat attributes (JSON patch syntax) eg. for a simple Wordpress contact-form</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
|
||||||
|
{
|
||||||
|
"fullName": "First Tester",
|
||||||
|
"name/personal": "First",
|
||||||
|
"name/surname": "Tester",
|
||||||
|
"organizations/org/name": "Test Organization",
|
||||||
|
"emails/work": "test.user@test-user.org",
|
||||||
|
"addresses/work/locality": "Test-Town",
|
||||||
|
"addresses/work/postcode": "12345",
|
||||||
|
"addresses/work/street": "Teststr. 123",
|
||||||
|
"addresses/work/country": "Germany",
|
||||||
|
"addresses/work/countryCode": "DE",
|
||||||
|
"phones/tel_work": "+49 123 4567890",
|
||||||
|
"online/url": "https://www.example.org/",
|
||||||
|
"notes/note": "This is a note.",
|
||||||
|
"egroupware.org:customfields/Test": "Content for Test"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: PUT request to update a resource</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PUT -d @- -H "Content-Type: application/json" --user <username>
|
||||||
|
{
|
||||||
|
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||||
|
"prodId": "EGroupware Addressbook 21.1.001",
|
||||||
|
"created": "2010-10-21T09:55:42Z",
|
||||||
|
"updated": "2014-06-02T14:45:24Z",
|
||||||
|
"name": [
|
||||||
|
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
|
||||||
|
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
|
||||||
|
],
|
||||||
|
"fullName": { "value": "Default Tester" },
|
||||||
|
....
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: PUT request with UID to update an existing resource or create it, if not exists</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -d @- -H "Content-Type: application/json" --user <username>
|
||||||
|
{
|
||||||
|
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||||
|
"prodId": "EGroupware Addressbook 21.1.001",
|
||||||
|
"created": "2010-10-21T09:55:42Z",
|
||||||
|
"updated": "2014-06-02T14:45:24Z",
|
||||||
|
"name": [
|
||||||
|
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
|
||||||
|
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
|
||||||
|
],
|
||||||
|
"fullName": { "value": "Default Tester" },
|
||||||
|
....
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
Update of an existing one:
|
||||||
|
```
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
```
|
||||||
|
New contact:
|
||||||
|
```
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
* **PATCH** request with a ```Content-Type: application/json``` header allow to modify a single resource by only specifying changed attributes as a [PatchObject](https://www.rfc-editor.org/rfc/rfc8984.html#type-PatchObject)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: PATCH request to modify a contact with partial data</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
|
||||||
|
{
|
||||||
|
"name": [
|
||||||
|
{
|
||||||
|
"@type": "NameComponent",
|
||||||
|
"type": "personal",
|
||||||
|
"value": "Testfirst"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "NameComponent",
|
||||||
|
"type": "surname",
|
||||||
|
"value": "Username"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fullName": "Testfirst Username",
|
||||||
|
"organizations/org/name": "Test-User.org",
|
||||||
|
"emails/work/email": "test.user@test-user.org"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
HTTP/1.1 204 No content
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
* **DELETE** requests delete single resources
|
||||||
|
|
||||||
|
* one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API
|
||||||
|
|
||||||
|
#### Implemented [changes from JsContact draft 08](https://github.com/rsto/draft-stepanek-jscontact/compare/draft-ietf-jmap-jscontact-08):
|
||||||
|
* localizedString type / object is removed in favor or regular String type and a [localizations object like in JsCalendar](https://datatracker.ietf.org/doc/html/rfc8984#section-4.6.1)
|
||||||
|
* [Vendor-specific Property Extensions and Values](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-1.3)
|
||||||
|
use ```<domain-name>:<name>``` like in JsCalendar
|
||||||
|
* top-level objects need a ```@type``` attribute with one of the following values:
|
||||||
|
```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```,
|
||||||
|
```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation```
|
383
doc/REST-CalDAV-CardDAV/Calendar.md
Normal file
383
doc/REST-CalDAV-CardDAV/Calendar.md
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
# EGroupware REST API for Calendar
|
||||||
|
|
||||||
|
Authentication is via Basic Auth with username and a password, or a token valid for:
|
||||||
|
- either just the given user or all users
|
||||||
|
- CalDAV/CardDAV Sync (REST API)
|
||||||
|
- Calendar application
|
||||||
|
|
||||||
|
Following RFCs / drafts used/planned for JSON encoding of resources
|
||||||
|
* [rfc8984: JSCalendar: A JSON Representation of Calendar Data](https://datatracker.ietf.org/doc/html/rfc8984)
|
||||||
|
|
||||||
|
### Supported request methods and examples
|
||||||
|
|
||||||
|
* **GET** to collections with an ```Accept: application/json``` header return all resources (similar to WebDAV PROPFIND)
|
||||||
|
<details>
|
||||||
|
<summary>Example: Getting all entries of a given users calendar</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
curl https://example.org/egroupware/groupdav.php/<username>/calendar/ -H "Accept: application/pretty+json" --user <username>
|
||||||
|
{
|
||||||
|
"responses": {
|
||||||
|
"/<username>/calendar/5695": {
|
||||||
|
"@type": "Event",
|
||||||
|
"prodId": "EGroupware Calendar 23.1.002",
|
||||||
|
"uid": "calendar-5695-34b52fc11cfa7e9acea5732210a53f48",
|
||||||
|
"sequence": "1",
|
||||||
|
"created": "2023-07-14T06:05:53Z",
|
||||||
|
"updated": "2023-07-14T08:00:04Z",
|
||||||
|
"title": "Test",
|
||||||
|
"start": "2023-07-14T10:00:00",
|
||||||
|
"timeZone": "Europe/Berlin",
|
||||||
|
"duration": "PT1H",
|
||||||
|
"participants": {
|
||||||
|
"5": {
|
||||||
|
"@type": "Participant",
|
||||||
|
"name": "Ralf Becker",
|
||||||
|
"email": "ralf@boulder.egroupware.org",
|
||||||
|
"kind": "individual",
|
||||||
|
"roles": {
|
||||||
|
"owner": true,
|
||||||
|
"chair": true
|
||||||
|
},
|
||||||
|
"participationStatus": "accepted"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "confirmed",
|
||||||
|
"priority": 5,
|
||||||
|
"privacy": "public"
|
||||||
|
},
|
||||||
|
"/<username>/calendar/5699": {
|
||||||
|
"@type": "Event",
|
||||||
|
"prodId": "EGroupware Calendar 23.1.002",
|
||||||
|
"uid": "calendar-5699-34b52fc11cfa7e9acea5732210a53f48",
|
||||||
|
"sequence": "5",
|
||||||
|
"created": "2023-07-24T10:06:23Z",
|
||||||
|
"updated": "2023-07-24T13:11:49Z",
|
||||||
|
"title": "Monday and Wednesday 13h",
|
||||||
|
"start": "2023-07-24T13:00:00",
|
||||||
|
"timeZone": "Europe/Berlin",
|
||||||
|
"duration": "PT1H",
|
||||||
|
"recurrenceRules": [
|
||||||
|
{
|
||||||
|
"@type": "RecurrenceRule",
|
||||||
|
"frequency": "weekly",
|
||||||
|
"until": "2023-08-30T13:00:00",
|
||||||
|
"byDay": {
|
||||||
|
"@type": "NDay",
|
||||||
|
"day": "mo,we"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recurrenceOverrides": {
|
||||||
|
"2023-07-31T13:00:00": { "excluded": true },
|
||||||
|
"2023-07-26T13:00:00": {
|
||||||
|
"sequence": "1",
|
||||||
|
"updated": "2023-07-24T11:39:44Z",
|
||||||
|
"title": "Monday und Wednesday 13h, but 26th 14h",
|
||||||
|
"start": "2023-07-26T14:00:00",
|
||||||
|
"description": "sdfasdf",
|
||||||
|
"showWithoutTime": null,
|
||||||
|
"categories": null,
|
||||||
|
"alerts": {
|
||||||
|
"64be4dc0-a044-4b27-b450-0026ac120002": {
|
||||||
|
"@type": "Alert",
|
||||||
|
"trigger": {
|
||||||
|
"@type": "OffsetTrigger",
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"64be4d1f-c958-4fbd-afc3-0026ac120002": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"participants": {
|
||||||
|
"5": {
|
||||||
|
"@type": "Participant",
|
||||||
|
"name": "Ralf Becker",
|
||||||
|
"email": "ralf@boulder.egroupware.org",
|
||||||
|
"kind": "individual",
|
||||||
|
"roles": {
|
||||||
|
"owner": true,
|
||||||
|
"chair": true
|
||||||
|
},
|
||||||
|
"participationStatus": "accepted"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"64be4d1f-c958-4fbd-afc3-0026ac120002": {
|
||||||
|
"@type": "Alert",
|
||||||
|
"trigger": {
|
||||||
|
"@type": "OffsetTrigger",
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "confirmed",
|
||||||
|
"priority": 5,
|
||||||
|
"privacy": "public"
|
||||||
|
},
|
||||||
|
"/<username>/calendar/5701": {
|
||||||
|
"@type": "Event",
|
||||||
|
"prodId": "EGroupware Calendar 23.1.002",
|
||||||
|
"uid": "calendar-5701-34b52fc11cfa7e9acea5732210a53f48",
|
||||||
|
"sequence": "1",
|
||||||
|
"created": "2023-07-24T12:31:58Z",
|
||||||
|
"updated": "2023-07-24T12:41:54Z",
|
||||||
|
"title": "Di und Do den ganzen Tag",
|
||||||
|
"start": "2023-07-25T00:00:00",
|
||||||
|
"timeZone": "Europe/Berlin",
|
||||||
|
"showWithoutTime": true,
|
||||||
|
"duration": "P1D",
|
||||||
|
"recurrenceRules": [
|
||||||
|
{
|
||||||
|
"@type": "RecurrenceRule",
|
||||||
|
"frequency": "weekly",
|
||||||
|
"until": "2023-08-03T00:00:00",
|
||||||
|
"byDay": {
|
||||||
|
"@type": "NDay",
|
||||||
|
"day": "tu,th"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recurrenceOverrides": {
|
||||||
|
"2023-07-27T00:00:00": {
|
||||||
|
"title": "Di und Do den ganzen Tag: AUSNAHME",
|
||||||
|
"start": "2023-07-27T00:00:00",
|
||||||
|
"description": "adsfads",
|
||||||
|
"sequence": null,
|
||||||
|
"categories": null,
|
||||||
|
"participants": {
|
||||||
|
"44": {
|
||||||
|
"@type": "Participant",
|
||||||
|
"name": "Birgit Becker",
|
||||||
|
"email": "birgit@boulder.egroupware.org",
|
||||||
|
"kind": "individual",
|
||||||
|
"roles": { "attendee": true },
|
||||||
|
"participationStatus": "needs-action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"64be7192-d4e4-4609-8c7a-004dac120002": {
|
||||||
|
"@type": "Alert",
|
||||||
|
"trigger": {
|
||||||
|
"@type": "OffsetTrigger",
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"64be6f3e-bc8c-4e78-9f96-004bac120002": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"freeBusyStatus": "free",
|
||||||
|
"participants": {
|
||||||
|
"5": {
|
||||||
|
"@type": "Participant",
|
||||||
|
"name": "Ralf Becker",
|
||||||
|
"email": "ralf@boulder.egroupware.org",
|
||||||
|
"kind": "individual",
|
||||||
|
"roles": {
|
||||||
|
"owner": true,
|
||||||
|
"chair": true
|
||||||
|
},
|
||||||
|
"participationStatus": "accepted"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"64be6f3e-bc8c-4e78-9f96-004bac120002": {
|
||||||
|
"@type": "Alert",
|
||||||
|
"trigger": {
|
||||||
|
"@type": "OffsetTrigger",
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "confirmed",
|
||||||
|
"priority": 5,
|
||||||
|
"privacy": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
following GET parameters are supported to customize the returned properties:
|
||||||
|
- props[]=<DAV-prop-name> eg. props[]=getetag to return only the ETAG (multiple DAV properties can be specified)
|
||||||
|
Default for addressbook collections is to only return address-data (JsContact), other collections return all props.
|
||||||
|
- sync-token=<token> to only request change since last sync-token, like rfc6578 sync-collection REPORT
|
||||||
|
- nresults=N limit number of responses (only for sync-collection / given sync-token parameter!)
|
||||||
|
this will return a "more-results"=true attribute and a new "sync-token" attribute to query for the next chunk
|
||||||
|
|
||||||
|
Examples: see addressbook
|
||||||
|
|
||||||
|
|
||||||
|
* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsContact or JsCalendar schema
|
||||||
|
<details>
|
||||||
|
<summary>Example: GET request for a single resource</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
curl 'https://example.org/egroupware/groupdav.php/addressbook/6502' -H "Accept: application/pretty+json" --user <username>
|
||||||
|
{
|
||||||
|
"@type": "Event",
|
||||||
|
"prodId": "EGroupware Calendar 23.1.002",
|
||||||
|
"uid": "calendar-5695-34b52fc11cfa7e9acea5732210a53f48",
|
||||||
|
"sequence": "1",
|
||||||
|
"created": "2023-07-14T06:05:53Z",
|
||||||
|
"updated": "2023-07-14T08:00:04Z",
|
||||||
|
"title": "Test",
|
||||||
|
"start": "2023-07-14T10:00:00",
|
||||||
|
"timeZone": "Europe/Berlin",
|
||||||
|
"duration": "PT1H",
|
||||||
|
"participants": {
|
||||||
|
"5": {
|
||||||
|
"@type": "Participant",
|
||||||
|
"name": "Ralf Becker",
|
||||||
|
"email": "ralf@boulder.egroupware.org",
|
||||||
|
"kind": "individual",
|
||||||
|
"roles": {
|
||||||
|
"owner": true,
|
||||||
|
"chair": true
|
||||||
|
},
|
||||||
|
"participationStatus": "accepted"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "confirmed",
|
||||||
|
"priority": 5,
|
||||||
|
"privacy": "public"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
* **POST** requests to collection with a ```Content-Type: application/json``` header add new entries in addressbook or calendar collections
|
||||||
|
(Location header in response gives URL of new resource)
|
||||||
|
<details>
|
||||||
|
<summary>Example: POST request to create a new resource and use "Prefer: return=representation" to get it fully expanded back</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/calendar/' -X POST -d @- -H "Content-Type: application/json" -H "Prefer: return=representation" --user <username>
|
||||||
|
{
|
||||||
|
"title": "Test 25th",
|
||||||
|
"start": "2023-07-25T10:00:00",
|
||||||
|
"timeZone": "Europe/Berlin",
|
||||||
|
"duration": "PT1H"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Content-Type: application/jscalendar+json;type=event;charset=utf-8
|
||||||
|
Location: /egroupware/groupdav.php/ralf/calendar/5704
|
||||||
|
ETag: "5704:0:1690209221"
|
||||||
|
Schedule-Tag: "5704:0"
|
||||||
|
X-WebDAV-Status: 201 Created
|
||||||
|
|
||||||
|
{
|
||||||
|
"@type":"Event",
|
||||||
|
"prodId":"EGroupware Calendar 23.1.002",
|
||||||
|
"uid":"urn:uuid:e2b7278b-d91a-47d1-85ee-19dd1fb9b315",
|
||||||
|
"created":"2023-07-24T14:33:41Z",
|
||||||
|
"updated":"2023-07-24T14:33:41Z",
|
||||||
|
"title":"Test 25th",
|
||||||
|
"start":"2023-07-25T10:00:00",
|
||||||
|
"timeZone":"Europe/Berlin",
|
||||||
|
"duration":"PT1H",
|
||||||
|
"participants":{
|
||||||
|
"5":{
|
||||||
|
"@type":"Participant",
|
||||||
|
"name":"Ralf Becker",
|
||||||
|
"email":"ralf@boulder.egroupware.org",
|
||||||
|
"kind":"individual",
|
||||||
|
"roles":{
|
||||||
|
"owner":true,
|
||||||
|
"chair":true
|
||||||
|
},
|
||||||
|
"participationStatus":"accepted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status":"confirmed",
|
||||||
|
"priority":5,
|
||||||
|
"privacy":"public"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: PUT request to update a resource</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PUT -d @- -H "Content-Type: application/json" --user <username>
|
||||||
|
{
|
||||||
|
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||||
|
"prodId": "EGroupware Addressbook 21.1.001",
|
||||||
|
"created": "2010-10-21T09:55:42Z",
|
||||||
|
"updated": "2014-06-02T14:45:24Z",
|
||||||
|
"name": [
|
||||||
|
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
|
||||||
|
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
|
||||||
|
],
|
||||||
|
"fullName": { "value": "Default Tester" },
|
||||||
|
....
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: PUT request with UID to update an existing resource or create it, if not exists</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/calendar/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -d @- -H "Content-Type: application/json" --user <username>
|
||||||
|
{
|
||||||
|
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
||||||
|
"title": "Testevent",
|
||||||
|
"start": "2023-07-24T12:00:00",
|
||||||
|
"timeZone": "Europe/Berlin",
|
||||||
|
"duration": "PT2H",
|
||||||
|
....
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
Update of an existing one:
|
||||||
|
```
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
```
|
||||||
|
New contact:
|
||||||
|
```
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Location: https://example.org/egroupware/groupdav.php/<username>/calendar/1234
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
* **PATCH** request with a ```Content-Type: application/json``` header allow to modify a single resource by only specifying changed attributes as a [PatchObject](https://www.rfc-editor.org/rfc/rfc8984.html#type-PatchObject)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example: PATCH request to modify an event with partial data</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/calendar/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
|
||||||
|
{
|
||||||
|
"title": "New title"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
HTTP/1.1 204 No content
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
* **DELETE** requests delete single resources
|
||||||
|
<details>
|
||||||
|
<summary>Example: Delete an existing event</summary>
|
||||||
|
> Please note: the "Accept: application/json" header is required, as the CalDAV server would return 404 NotFound as the url does NOT end with .ics
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -i 'https://example.org/egroupware/groupdav.php/<username>/calendar/1234' -X DELETE -H "Accept: application/json" --user <username>
|
||||||
|
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
* one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON e.g. for debugging and exploring the API
|
@ -36,513 +36,26 @@ One can use the following URLs relative (!) to https://example.org/egroupware/gr
|
|||||||
Shared addressbooks or calendars are only shown in the users home-set, if he subscribed to it via his CalDAV preferences!
|
Shared addressbooks or calendars are only shown in the users home-set, if he subscribed to it via his CalDAV preferences!
|
||||||
|
|
||||||
Calling one of the above collections with a GET request / regular browser generates an automatic index
|
Calling one of the above collections with a GET request / regular browser generates an automatic index
|
||||||
from the data of an allprop PROPFIND, allow browsing CalDAV/CardDAV tree with a regular browser.
|
from the data of an ```allprop``` PROPFIND, allow browsing CalDAV/CardDAV tree with a regular browser.
|
||||||
|
|
||||||
## REST API: using EGroupware CalDAV/CardDAV server with JSON
|
## REST API: using EGroupware CalDAV/CardDAV server with JSON
|
||||||
> currently implemented only for contacts and calendar (without recurring events)!
|
- [Addressbook](Addressbook.md)
|
||||||
|
- [Calendar](Calendar.md) (currently recurring events are readonly, they are returned but can not be created or modified)
|
||||||
|
- [Mail](Mail.md) (currently only sending mails or opening interactive compose windows)
|
||||||
|
|
||||||
Following RFCs / drafts used/planned for JSON encoding of ressources
|
Following RFCs / drafts used/planned for JSON encoding of resources
|
||||||
* [draft-ietf-jmap-jscontact: JSContact: A JSON Representation of Contact Data](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact)
|
* [draft-ietf-jmap-jscontact: JSContact: A JSON Representation of Contact Data](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact)
|
||||||
([* see at end of document](#implemented-changes-from-jscontact-draft-08))
|
([* see at end of document](#implemented-changes-from-jscontact-draft-08))
|
||||||
* [draft-ietf-jmap-jscontact-vcard: JSContact: Converting from and to vCard](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-vcard/)
|
* [draft-ietf-jmap-jscontact-vcard: JSContact: Converting from and to vCard](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-vcard/)
|
||||||
* [rfc8984: JSCalendar: A JSON Representation of Calendar Data](https://datatracker.ietf.org/doc/html/rfc8984)
|
* [rfc8984: JSCalendar: A JSON Representation of Calendar Data](https://datatracker.ietf.org/doc/html/rfc8984)
|
||||||
|
|
||||||
### Supported request methods and examples
|
|
||||||
|
|
||||||
* **GET** to collections with an ```Accept: application/json``` header return all resources (similar to WebDAV PROPFIND)
|
|
||||||
<details>
|
|
||||||
<summary>Example: Getting all entries of a given users addessbook</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Accept: application/pretty+json" --user <username>
|
|
||||||
{
|
|
||||||
"responses": {
|
|
||||||
"/<username>/addressbook/1833": {
|
|
||||||
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
|
||||||
"prodId": "EGroupware Addressbook 21.1.001",
|
|
||||||
"created": "2010-10-21T09:55:42Z",
|
|
||||||
"updated": "2014-06-02T14:45:24Z",
|
|
||||||
"name": [
|
|
||||||
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
|
|
||||||
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
|
|
||||||
],
|
|
||||||
"fullName": { "value": "Default Tester" },
|
|
||||||
"organizations": {
|
|
||||||
"org": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "default.org",
|
|
||||||
"units": {
|
|
||||||
"org_unit": "department.default.org"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"emails": {
|
|
||||||
"work": { "@type": "EmailAddress", "email": "test@test.com", "contexts": { "work": true }, "pref": 1 }
|
|
||||||
},
|
|
||||||
"phones": {
|
|
||||||
"tel_work": { "@type": "Phone", "phone": "+49 123 4567890", "pref": 1, "features": { "voice": true }, "contexts": { "work": true } },
|
|
||||||
"tel_cell": { "@type": "Phone", "phone": "012 3723567", "features": { "cell": true }, "contexts": { "work": true } }
|
|
||||||
},
|
|
||||||
"online": {
|
|
||||||
"url": { "@type": "Resource", "resource": "https://www.test.com/", "type": "uri", "contexts": { "work": true } }
|
|
||||||
},
|
|
||||||
"notes": [
|
|
||||||
"Test test TEST\n\\server\\share\n\\\nother\nblah"
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"/<username>/addressbook/list-36": {
|
|
||||||
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
|
|
||||||
"name": "Example distribution list",
|
|
||||||
"card": {
|
|
||||||
"uid": "urn:uuid:dfa5cac5-987b-448b-85d7-6c8b529a835c",
|
|
||||||
"prodId": "EGroupware Addressbook 21.1.001",
|
|
||||||
"updated": "2018-04-11T14:46:43Z",
|
|
||||||
"fullName": { "value": "Example distribution list" }
|
|
||||||
},
|
|
||||||
"members": {
|
|
||||||
"5638-8623c4830472a8ede9f9f8b30d435ea4": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
following GET parameters are supported to customize the returned properties:
|
|
||||||
- props[]=<DAV-prop-name> eg. props[]=getetag to return only the ETAG (multiple DAV properties can be specified)
|
|
||||||
Default for addressbook collections is to only return address-data (JsContact), other collections return all props.
|
|
||||||
- sync-token=<token> to only request change since last sync-token, like rfc6578 sync-collection REPORT
|
|
||||||
- nresults=N limit number of responses (only for sync-collection / given sync-token parameter!)
|
|
||||||
this will return a "more-results"=true attribute and a new "sync-token" attribute to query for the next chunk
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example: Getting just ETAGs and displayname of all contacts in a given AB</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/?props[]=getetag&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
|
||||||
{
|
|
||||||
"responses": {
|
|
||||||
"/addressbook/1833": {
|
|
||||||
"displayname": "Default Tester",
|
|
||||||
"getetag": "\"1833:24\""
|
|
||||||
},
|
|
||||||
"/addressbook/1838": {
|
|
||||||
"displayname": "Test Tester",
|
|
||||||
"getetag": "\"1838:19\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example: Start using a sync-token to get only changed entries since last sync</summary>
|
|
||||||
|
|
||||||
#### Initial request with empty sync-token and only requesting 10 entries per chunk:
|
|
||||||
```
|
|
||||||
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
|
||||||
{
|
|
||||||
"responses": {
|
|
||||||
"/addressbook/2050": "Frau Margot Test-Notifikation",
|
|
||||||
"/addressbook/2384": "Test Tester",
|
|
||||||
"/addressbook/5462": "Margot Testgedöns",
|
|
||||||
"/addressbook/2380": "Frau Test Defaulterin",
|
|
||||||
"/addressbook/5474": "Noch ein Neuer",
|
|
||||||
"/addressbook/5575": "Mr New Name",
|
|
||||||
"/addressbook/5461": "Herr Hugo Kurt Müller Senior",
|
|
||||||
"/addressbook/5601": "Steve Jobs",
|
|
||||||
"/addressbook/5603": "Ralf Becker",
|
|
||||||
"/addressbook/1838": "Test Tester"
|
|
||||||
},
|
|
||||||
"more-results": true,
|
|
||||||
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1400867824"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
#### Requesting next chunk:
|
|
||||||
```
|
|
||||||
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824&nresults=10&props[]=displayname' -H "Accept: application/pretty+json" --user <username>
|
|
||||||
{
|
|
||||||
"responses": {
|
|
||||||
"/addressbook/1833": "Default Tester",
|
|
||||||
"/addressbook/5597": "Neuer Testschnuffi",
|
|
||||||
"/addressbook/5593": "Muster Max",
|
|
||||||
"/addressbook/5628": "2. Test Contact",
|
|
||||||
"/addressbook/5629": "Testen Tester",
|
|
||||||
"/addressbook/5630": "Testen Tester",
|
|
||||||
"/addressbook/5633": "Testen Tester",
|
|
||||||
"/addressbook/5635": "Test4 Tester",
|
|
||||||
"/addressbook/5638": "Test Kontakt",
|
|
||||||
"/addressbook/5636": "Test Default"
|
|
||||||
},
|
|
||||||
"more-results": true,
|
|
||||||
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1427103057"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example: Requesting only changes since last sync</summary>
|
|
||||||
|
|
||||||
#### ```sync-token``` from last sync need to be specified (note the null for a deleted resource!)
|
|
||||||
```
|
|
||||||
curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https://example.org/egroupware/groupdav.php/addressbook/1400867824' -H "Accept: application/pretty+json" --user <username>
|
|
||||||
{
|
|
||||||
"responses": {
|
|
||||||
"/addressbook/5597": null,
|
|
||||||
"/addressbook/5593": {
|
|
||||||
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
|
||||||
"prodId": "EGroupware Addressbook 21.1.001",
|
|
||||||
"created": "2010-10-21T09:55:42Z",
|
|
||||||
"updated": "2014-06-02T14:45:24Z",
|
|
||||||
"name": [
|
|
||||||
{ "@type": "NameComponent", "type": "personal", "value": "Default" },
|
|
||||||
{ "@type": "NameComponent", "type": "surname", "value": "Tester" }
|
|
||||||
],
|
|
||||||
"fullName": "Default Tester",
|
|
||||||
....
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sync-token": "https://example.org/egroupware/groupdav.php/addressbook/1427103057"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
* **GET** requests with an ```Accept: application/json``` header can be used to retrieve single resources / JsContact or JsCalendar schema
|
|
||||||
<details>
|
|
||||||
<summary>Example: GET request for a single resource showcasing available fieldes</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
curl 'https://example.org/egroupware/groupdav.php/addressbook/6502' -H "Accept: application/pretty+json" --user <username>
|
|
||||||
{
|
|
||||||
"uid": "addressbook-6502-8623c4830472a8ede9f9f8b30d435ea4",
|
|
||||||
"prodId": "EGroupware Addressbook 21.1.003",
|
|
||||||
"created": "2022-12-14T13:35:02Z",
|
|
||||||
"updated": "2022-12-14T13:39:14Z",
|
|
||||||
"kind": "individual",
|
|
||||||
"name": [
|
|
||||||
{ "@type": "NameComponent", "type": "prefix", "value": "Prefix/Title" },
|
|
||||||
{ "@type": "NameComponent", "type": "personal", "value": "Frist" },
|
|
||||||
{ "@type": "NameComponent", "type": "additional", "value": "Middle" },
|
|
||||||
{ "@type": "NameComponent", "type": "surname", "value": "Last" },
|
|
||||||
{ "@type": "NameComponent", "type": "suffix", "value": "Postfix" }
|
|
||||||
],
|
|
||||||
"fullName": "Prefix/Title Frist Middle Last Postfix",
|
|
||||||
"organizations": {
|
|
||||||
"org": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "Organisation",
|
|
||||||
"units": { "org_unit": "Department" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"titles": {
|
|
||||||
"title": {
|
|
||||||
"@type": "Title",
|
|
||||||
"title": "Postion",
|
|
||||||
"organization": "org"
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"@type": "Title",
|
|
||||||
"title": "Occupation",
|
|
||||||
"organization": "org"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"emails": {
|
|
||||||
"work": {
|
|
||||||
"@type": "EmailAddress",
|
|
||||||
"email": "email@example.org",
|
|
||||||
"contexts": { "work": true },
|
|
||||||
"pref": 1
|
|
||||||
},
|
|
||||||
"private": {
|
|
||||||
"@type": "EmailAddress",
|
|
||||||
"email": "private.email@example.org",
|
|
||||||
"contexts": { "private": true }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"phones": {
|
|
||||||
"tel_work": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "voice": true },
|
|
||||||
"contexts": { "work": true }
|
|
||||||
},
|
|
||||||
"tel_cell": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "cell": true },
|
|
||||||
"contexts": { "work": true }
|
|
||||||
},
|
|
||||||
"tel_fax": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "fax": true },
|
|
||||||
"contexts": { "work": true }
|
|
||||||
},
|
|
||||||
"tel_assistent": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "voice": true },
|
|
||||||
"contexts": { "assistant": true }
|
|
||||||
},
|
|
||||||
"tel_car": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "voice": true },
|
|
||||||
"contexts": { "car": true }
|
|
||||||
},
|
|
||||||
"tel_pager": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "pager": true },
|
|
||||||
"contexts": { "work": true }
|
|
||||||
},
|
|
||||||
"tel_home": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "voice": true },
|
|
||||||
"contexts": { "private": true }
|
|
||||||
},
|
|
||||||
"tel_fax_home": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "fax": true },
|
|
||||||
"contexts": { "private": true }
|
|
||||||
},
|
|
||||||
"tel_cell_private": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "cell": true },
|
|
||||||
"contexts": { "private": true }
|
|
||||||
},
|
|
||||||
"tel_other": {
|
|
||||||
"@type": "Phone",
|
|
||||||
"phone": "+1(234)5678901",
|
|
||||||
"features": { "voice": true },
|
|
||||||
"contexts": { "work": true }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"online": {
|
|
||||||
"url": {
|
|
||||||
"@type": "Resource",
|
|
||||||
"resource": "https://example.org",
|
|
||||||
"type": "uri",
|
|
||||||
"contexts": { "work": true }
|
|
||||||
},
|
|
||||||
"url_home": {
|
|
||||||
"@type": "Resource",
|
|
||||||
"resource": "https://private.example.org",
|
|
||||||
"type": "uri",
|
|
||||||
"contexts": { "private": true }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"addresses": {
|
|
||||||
"work": {
|
|
||||||
"@type": "Address",
|
|
||||||
"locality": "City",
|
|
||||||
"region": "Rheinland-Pfalz",
|
|
||||||
"country": "DEUTSCHLAND",
|
|
||||||
"postcode": "12345",
|
|
||||||
"countryCode": "DE",
|
|
||||||
"street": [
|
|
||||||
{ "@type": "StreetComponent", "type": "name", "value": "Street" },
|
|
||||||
{ "@type": "StreetComponent", "type": "separator", "value": "\n" },
|
|
||||||
{ "@type": "StreetComponent", "type": "name", "value": "Street2" ],
|
|
||||||
"contexts": { "work": true },
|
|
||||||
"pref": 1
|
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"@type": "Address",
|
|
||||||
"locality": "PrivateCity",
|
|
||||||
"country": "DEUTSCHLAND",
|
|
||||||
"postcode": "12345",
|
|
||||||
"countryCode": "DE",
|
|
||||||
"street": [
|
|
||||||
{ "@type": "StreetComponent", "type": "name", "value": "PrivateStreet" },
|
|
||||||
{ "@type": "StreetComponent", "type": "separator", "value": "\n" },
|
|
||||||
{ "@type": "StreetComponent", "type": "name", "value": "PrivateStreet2" }
|
|
||||||
],
|
|
||||||
"contexts": { "home": true }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"photos": {
|
|
||||||
"photo": {
|
|
||||||
"@type": "File",
|
|
||||||
"href": "https://boulder.egroupware.org/egroupware/api/avatar.php?contact_id=6502&lavatar=1&etag=0",
|
|
||||||
"mediaType": "image/jpeg"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"anniversaries": {
|
|
||||||
"bday": {
|
|
||||||
"@type": "Anniversary",
|
|
||||||
"type": "birth",
|
|
||||||
"date": "2022-12-14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"categories": {
|
|
||||||
"Kategorie": true,
|
|
||||||
"My Contacts": true
|
|
||||||
},
|
|
||||||
"egroupware.org:assistant": "Assistent"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
* **POST** requests to collection with a ```Content-Type: application/json``` header add new entries in addressbook or calendar collections
|
|
||||||
(Location header in response gives URL of new resource)
|
|
||||||
<details>
|
|
||||||
<summary>Example: POST request to create a new resource</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
|
|
||||||
{
|
|
||||||
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
|
||||||
"prodId": "EGroupware Addressbook 21.1.001",
|
|
||||||
"created": "2010-10-21T09:55:42Z",
|
|
||||||
"updated": "2014-06-02T14:45:24Z",
|
|
||||||
"name": [
|
|
||||||
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
|
|
||||||
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
|
|
||||||
],
|
|
||||||
"fullName": { "value": "Default Tester" },
|
|
||||||
....
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
HTTP/1.1 201 Created
|
|
||||||
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example: POST request to create a new resource using flat attributes (JSON patch syntax) eg. for a simple Wordpress contact-form</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/' -X POST -d @- -H "Content-Type: application/json" --user <username>
|
|
||||||
{
|
|
||||||
"fullName": "First Tester",
|
|
||||||
"name/personal": "First",
|
|
||||||
"name/surname": "Tester",
|
|
||||||
"organizations/org/name": "Test Organization",
|
|
||||||
"emails/work": "test.user@test-user.org",
|
|
||||||
"addresses/work/locality": "Test-Town",
|
|
||||||
"addresses/work/postcode": "12345",
|
|
||||||
"addresses/work/street": "Teststr. 123",
|
|
||||||
"addresses/work/country": "Germany",
|
|
||||||
"addresses/work/countryCode": "DE",
|
|
||||||
"phones/tel_work": "+49 123 4567890",
|
|
||||||
"online/url": "https://www.example.org/",
|
|
||||||
"notes/note": "This is a note.",
|
|
||||||
"egroupware.org:customfields/Test": "Content for Test"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
HTTP/1.1 201 Created
|
|
||||||
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
* **PUT** requests with a ```Content-Type: application/json``` header allow modifying single resources (requires to specify all attributes!)
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example: PUT request to update a resource</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PUT -d @- -H "Content-Type: application/json" --user <username>
|
|
||||||
{
|
|
||||||
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
|
||||||
"prodId": "EGroupware Addressbook 21.1.001",
|
|
||||||
"created": "2010-10-21T09:55:42Z",
|
|
||||||
"updated": "2014-06-02T14:45:24Z",
|
|
||||||
"name": [
|
|
||||||
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
|
|
||||||
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
|
|
||||||
],
|
|
||||||
"fullName": { "value": "Default Tester" },
|
|
||||||
....
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
HTTP/1.1 204 No Content
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example: PUT request with UID to update an existing resource or create it, if not exists</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/5638-8623c4830472a8ede9f9f8b30d435ea4' -X PUT -d @- -H "Content-Type: application/json" --user <username>
|
|
||||||
{
|
|
||||||
"uid": "5638-8623c4830472a8ede9f9f8b30d435ea4",
|
|
||||||
"prodId": "EGroupware Addressbook 21.1.001",
|
|
||||||
"created": "2010-10-21T09:55:42Z",
|
|
||||||
"updated": "2014-06-02T14:45:24Z",
|
|
||||||
"name": [
|
|
||||||
{ "type": "@type": "NameComponent", "personal", "value": "Default" },
|
|
||||||
{ "type": "@type": "NameComponent", "surname", "value": "Tester" }
|
|
||||||
],
|
|
||||||
"fullName": { "value": "Default Tester" },
|
|
||||||
....
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
Update of an existing one:
|
|
||||||
```
|
|
||||||
HTTP/1.1 204 No Content
|
|
||||||
```
|
|
||||||
New contact:
|
|
||||||
```
|
|
||||||
HTTP/1.1 201 Created
|
|
||||||
Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/1234
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
* **PATCH** request with a ```Content-Type: application/json``` header allow to modify a single resource by only specifying changed attributes as a [PatchObject](https://www.rfc-editor.org/rfc/rfc8984.html#type-PatchObject)
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example: PATCH request to modify a contact with partial data</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/addressbook/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
|
|
||||||
{
|
|
||||||
"name": [
|
|
||||||
{
|
|
||||||
"@type": "NameComponent",
|
|
||||||
"type": "personal",
|
|
||||||
"value": "Testfirst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "NameComponent",
|
|
||||||
"type": "surname",
|
|
||||||
"value": "Username"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"fullName": "Testfirst Username",
|
|
||||||
"organizations/org/name": "Test-User.org",
|
|
||||||
"emails/work/email": "test.user@test-user.org"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
HTTP/1.1 204 No content
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
* **DELETE** requests delete single resources
|
|
||||||
|
|
||||||
* one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API
|
|
||||||
|
|
||||||
#### Implemented [changes from JsContact draft 08](https://github.com/rsto/draft-stepanek-jscontact/compare/draft-ietf-jmap-jscontact-08):
|
|
||||||
* localizedString type / object is removed in favor or regular String type and a [localizations object like in JsCalendar](https://datatracker.ietf.org/doc/html/rfc8984#section-4.6.1)
|
|
||||||
* [Vendor-specific Property Extensions and Values](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-1.3)
|
|
||||||
use ```<domain-name>:<name>``` like in JsCalendar
|
|
||||||
* top-level objects need a ```@type``` attribute with one of the following values:
|
|
||||||
```NameComponent```, ```Organization```, ```Title```, ```Phone```, ```Resource```, ```File```, ```ContactLanguage```,
|
|
||||||
```Address```, ```StreetComponent```, ```Anniversary```, ```PersonalInformation```
|
|
||||||
|
|
||||||
### ToDos
|
### ToDos
|
||||||
- [x] Addressbook
|
- [x] Addressbook
|
||||||
- [ ] update of photos, keys, attachments
|
- [ ] update of photos, keys, attachments
|
||||||
- [ ] InfoLog
|
- [ ] InfoLog
|
||||||
- [X] Calendar
|
- [X] Calendar (recurring events and alarms are readonly)
|
||||||
|
- [ ] support creating and modifying recurring events and alarms
|
||||||
|
- [X] Mail
|
||||||
|
- [ ] querying received mails
|
||||||
- [ ] relatedTo / links
|
- [ ] relatedTo / links
|
||||||
- [ ] storing not native supported attributes eg. localization
|
- [ ] storing not native supported attributes eg. localization
|
Loading…
Reference in New Issue
Block a user