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 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
|
||||
* @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)))
|
||||
{
|
||||
throw new Api\Exception\NotFound();
|
||||
}
|
||||
$data = array_filter([
|
||||
$data = [
|
||||
self::AT_TYPE => self::TYPE_EVENT,
|
||||
'prodId' => 'EGroupware Calendar '.$GLOBALS['egw_info']['apps']['api']['version'],
|
||||
'uid' => self::uid($event['uid']),
|
||||
@ -54,16 +55,25 @@ class JsCalendar
|
||||
'timeZone' => $event['tzid'],
|
||||
'showWithoutTime' => $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
|
||||
'description' => $event['description'],
|
||||
'participants' => self::Participants($event),
|
||||
'alerts' => self::Alerts($event['alarm']),
|
||||
'status' => empty($event['deleted']) ? 'confirmed' : 'cancelled', // we have no "tentative" event-status (only participants)!
|
||||
'priority' => self::Priority($event['priority']),
|
||||
'categories' => self::categories($event['category']),
|
||||
'privacy' => $event['public'] ? 'public' : 'private',
|
||||
'alerts' => self::Alerts($event['alarms']),
|
||||
'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)
|
||||
{
|
||||
return Api\CalDAV::json_encode($data, $encode === "pretty");
|
||||
@ -149,7 +159,13 @@ class JsCalendar
|
||||
break;
|
||||
|
||||
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;
|
||||
|
||||
case 'categories':
|
||||
@ -174,6 +190,13 @@ class JsCalendar
|
||||
catch (\Throwable $e) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -538,13 +561,15 @@ class JsCalendar
|
||||
$info = self::getCalendar()->resource_info($uid) ?: [];
|
||||
switch($info['type'] ?? $info['app'])
|
||||
{
|
||||
case 'user':
|
||||
case 'e': // email
|
||||
case 'c': // contact
|
||||
case 'u': // user
|
||||
$info['kind'] = 'individual';
|
||||
break;
|
||||
case 'group':
|
||||
case 'g':
|
||||
$info['kind'] = 'group';
|
||||
break;
|
||||
case 'resources':
|
||||
case 'r':
|
||||
$info['kind'] = Api\CalDAV\Principals::resource_is_location($user_id) ? 'location' : 'resource';
|
||||
break;
|
||||
}
|
||||
@ -552,7 +577,7 @@ class JsCalendar
|
||||
catch (\Exception $e) {
|
||||
$info = [];
|
||||
}
|
||||
$participant = [
|
||||
$participant = array_filter([
|
||||
self::AT_TYPE => self::TYPE_PARTICIPANT,
|
||||
'name' => $info['name'] ?? null,
|
||||
'email' => $info['email'] ?? null,
|
||||
@ -565,7 +590,7 @@ class JsCalendar
|
||||
'informational' => $role === 'NON-PARTICIPANT',
|
||||
]),
|
||||
'participationStatus' => $status2jscal[$status],
|
||||
];
|
||||
]);
|
||||
$participants[$uid] = $participant;
|
||||
}
|
||||
|
||||
@ -612,22 +637,122 @@ class JsCalendar
|
||||
return $priority_egw2jscal[$priority];
|
||||
}
|
||||
|
||||
const TYPE_RECURRENCE_RULE = 'RecurrenceRule';
|
||||
const TYPE_NDAY = 'NDay';
|
||||
|
||||
/**
|
||||
* 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 $data JSCalendar representation of event to calculate overrides
|
||||
* @param array $exceptions exceptions
|
||||
* @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
|
||||
*
|
||||
* @TODO
|
||||
* @param array|null $alarms
|
||||
* @return array
|
||||
*/
|
||||
@ -636,7 +761,19 @@ class JsCalendar
|
||||
$alerts = [];
|
||||
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;
|
||||
}
|
||||
|
@ -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 !!!
|
||||
*
|
||||
@ -2191,9 +2191,9 @@ class calendar_bo
|
||||
$entry = $this->read($id, $recur_date, true, 'server');
|
||||
}
|
||||
$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;
|
||||
}
|
||||
|
||||
|
@ -383,10 +383,11 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
$events = null;
|
||||
$is_jscalendar = Api\CalDAV::isJSON();
|
||||
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,
|
||||
'num_rows' => self::CHUNK_SIZE,
|
||||
])); ++$chunk)
|
||||
'date_format' => $is_jscalendar ? 'object' : 'ts',
|
||||
]+$filter)); ++$chunk)
|
||||
{
|
||||
foreach($events as $event)
|
||||
{
|
||||
@ -441,11 +442,10 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
//error_log(__FILE__ . __METHOD__ . "Calendar Data : $calendar_data");
|
||||
if ($calendar_data)
|
||||
{
|
||||
$content = $is_jscalendar ? Api\CalDAV\JsCalendar::getJsEvent($event, false) :
|
||||
$this->iCal($event, $filter['users'],
|
||||
$content = $this->iCal($event, $filter['users'],
|
||||
strpos($path, '/inbox/') !== false ? 'REQUEST' : null,
|
||||
!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['calendar-data'] = Api\CalDAV::mkprop(Api\CalDAV::CALDAV,'calendar-data',$content);
|
||||
}
|
||||
@ -578,10 +578,12 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
}
|
||||
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));
|
||||
return $props ? $props : '';
|
||||
return $props ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -763,12 +765,12 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
// jsEvent or iCal
|
||||
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';
|
||||
}
|
||||
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';
|
||||
}
|
||||
header('Content-Encoding: identity');
|
||||
@ -784,16 +786,17 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
/**
|
||||
* 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 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 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;
|
||||
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
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
if (isset($json))
|
||||
{
|
||||
return Api\CalDAV\JsCalendar::JsEvent($events[0], $json, array_slice($events, 1));
|
||||
}
|
||||
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
|
||||
* @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();
|
||||
|
||||
@ -880,7 +887,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
'query' => array('cal_uid' => $uid),
|
||||
'filter' => 'owner', // return all possible entries
|
||||
'daywise' => false,
|
||||
'date_format' => 'server',
|
||||
'date_format' => $date_format,
|
||||
'cfs' => array(), // read cfs as we use them to store X- attributes
|
||||
);
|
||||
if (is_array($expand)) $params += $expand;
|
||||
@ -891,7 +898,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
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();
|
||||
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 (!isset($master)) $master = $bo->read($uid);
|
||||
if (!isset($master)) $master = $bo->read($uid, null, false, $date_format);
|
||||
|
||||
foreach($events as $k => &$recurrence)
|
||||
{
|
||||
@ -912,7 +919,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
if ($master && $recurrence['reference'] && $recurrence['reference'] != $master['id'])
|
||||
{
|
||||
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
|
||||
{
|
||||
@ -929,7 +936,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
$recurrence['participants'][$user] = 'R';
|
||||
}
|
||||
//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
|
||||
// in the whole resource / VCALENDAR component
|
||||
// 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;
|
||||
|
||||
$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 (!($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
|
||||
(!$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));
|
||||
return $retval;
|
||||
|
@ -538,9 +538,9 @@ class calendar_rrule implements Iterator
|
||||
$previous = clone $this->current;
|
||||
$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
|
||||
// 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'))
|
||||
{
|
||||
$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!
|
||||
|
||||
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
|
||||
> 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)
|
||||
([* 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```
|
||||
|
||||
### ToDos
|
||||
- [x] Addressbook
|
||||
- [ ] update of photos, keys, attachments
|
||||
- [ ] 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
|
||||
- [ ] storing not native supported attributes eg. localization
|
Loading…
Reference in New Issue
Block a user