mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-25 01:13:25 +01:00
Contacts REST API:
- implement missing PUT/POST of distribution list / CardGroups - implement changes from next JsContact draft: + string plus extra localizations attribute instead of localizedString object + use "<domain>:<name>" for vendor attributes - add/parse urn:uuid: prefix if UID is a UUID
This commit is contained in:
parent
0a75520a01
commit
82c8ed51d2
@ -61,6 +61,11 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
*/
|
||||
var $home_set_pref;
|
||||
|
||||
/**
|
||||
* Prefix for JsCardGroup id
|
||||
*/
|
||||
const JS_CARDGROUP_ID_PREFIX = 'list-';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@ -350,7 +355,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
{
|
||||
foreach($lists as $list)
|
||||
{
|
||||
$list[self::$path_attr] = $is_jscontact ? 'list-'.$list['list_id'] : $list['list_carddav_name'];
|
||||
$list[self::$path_attr] = $is_jscontact ? self::JS_CARDGROUP_ID_PREFIX.$list['list_id'] : $list['list_carddav_name'];
|
||||
$etag = $list['list_id'].':'.$list['list_etag'];
|
||||
// for all-in-one addressbook, add selected ABs to etag
|
||||
if (isset($filter['owner']) && is_array($filter['owner']))
|
||||
@ -637,9 +642,33 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
return $oldContact;
|
||||
}
|
||||
|
||||
if (Api\CalDAV::isJSON())
|
||||
$type = null;
|
||||
if (($is_json=Api\CalDAV::isJSON($type)))
|
||||
{
|
||||
$contact = JsContact::parseJsCard($options['content']);
|
||||
if (strpos($type, JsContact::MIME_TYPE_JSCARD) === false && strpos($type, JsContact::MIME_TYPE_JSCARDGROUP) === false)
|
||||
{
|
||||
if (!empty($id))
|
||||
{
|
||||
$type = strpos($id, self::JS_CARDGROUP_ID_PREFIX) === 0 ? JsContact::MIME_TYPE_JSCARDGROUP : JsContact::MIME_TYPE_JSCARD;
|
||||
}
|
||||
else
|
||||
{
|
||||
$json = json_decode($options['content'], true);
|
||||
$type = is_array($json['members']) ? JsContact::MIME_TYPE_JSCARDGROUP : JsContact::MIME_TYPE_JSCARD;
|
||||
}
|
||||
}
|
||||
$contact = $type === JsContact::MIME_TYPE_JSCARD ?
|
||||
JsContact::parseJsCard($options['content']) : JsContact::parseJsCardGroup($options['content']);
|
||||
|
||||
if (!empty($id) && strpos($id, self::JS_CARDGROUP_ID_PREFIX) === 0)
|
||||
{
|
||||
$id = substr($id, strlen(self::JS_CARDGROUP_ID_PREFIX));
|
||||
}
|
||||
elseif (empty($id))
|
||||
{
|
||||
$contact['cardav_name'] = $contact['uid'].'.vcf';
|
||||
$contact['owner'] = $user;
|
||||
}
|
||||
|
||||
/* uncomment to return parsed data for testing
|
||||
header('Content-Type: application/json');
|
||||
@ -687,7 +716,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
$contactId = -1;
|
||||
$retval = '201 Created';
|
||||
}
|
||||
$is_group = $contact['##X-ADDRESSBOOKSERVER-KIND'] == 'group';
|
||||
$is_group = isset($type) && $type === JsContact::MIME_TYPE_JSCARDGROUP || $contact['##X-ADDRESSBOOKSERVER-KIND'] === 'group';
|
||||
if ($oldContact && $is_group !== isset($oldContact['list_id']))
|
||||
{
|
||||
throw new Api\Exception\AssertionFailed(__METHOD__."(,'$id',$user,'$prefix') can contact into group or visa-versa!");
|
||||
@ -756,7 +785,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
if ($this->http_if_match) $contact['etag'] = self::etag2value($this->http_if_match);
|
||||
|
||||
// ignore photo for JSON/REST, it's not yet supported
|
||||
$contact['photo_unchanged'] = Api\CalDAV::isJSON(); //false; // photo needs saving
|
||||
$contact['photo_unchanged'] = $is_json; //false; // photo needs saving
|
||||
if (!($save_ok = $is_group ? $this->save_group($contact, $oldContact) : $this->bo->save($contact)))
|
||||
{
|
||||
if ($this->debug) error_log(__METHOD__."(,$id) save(".array2string($contact).") failed, Ok=$save_ok");
|
||||
@ -775,7 +804,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
{
|
||||
if (($contact = $this->bo->read_list($save_ok)))
|
||||
{
|
||||
// re-read group to get correct etag (not dublicate etag code here)
|
||||
// re-read group to get correct etag (not duplicate etag code here)
|
||||
$contact = $this->read($contact['list_'.self::$path_attr], $options['path']);
|
||||
}
|
||||
}
|
||||
@ -787,14 +816,17 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
}
|
||||
|
||||
// send evtl. necessary response headers: Location, etag, ...
|
||||
$this->put_response_headers($contact, $options['path'], $retval, self::$path_attr != 'id');
|
||||
$this->put_response_headers($contact, $options['path'], $retval,
|
||||
// JSON uses 'id', while CardDAV uses carddav_name !== 'id'
|
||||
(self::$path_attr !== 'id') === !$is_json, null,
|
||||
$is_group && $is_json ? self::JS_CARDGROUP_ID_PREFIX : '');
|
||||
|
||||
if ($this->debug > 1) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($retval));
|
||||
return $retval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save distribition-list / group
|
||||
* Save distribution-list / group
|
||||
*
|
||||
* @param array $contact
|
||||
* @param array|false $oldContact
|
||||
@ -813,18 +845,21 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
$contact['owner'], null, $data)))
|
||||
{
|
||||
// update members given in $contact['##X-ADDRESSBOOKSERVER-MEMBER']
|
||||
$new_members = $contact['##X-ADDRESSBOOKSERVER-MEMBER'];
|
||||
if ($new_members[1] == ':' && ($n = unserialize($new_members)))
|
||||
$new_members = $contact['members'] ?: $contact['##X-ADDRESSBOOKSERVER-MEMBER'];
|
||||
if (is_string($new_members) && $new_members[1] === ':' && ($n = unserialize($new_members)))
|
||||
{
|
||||
$new_members = $n['values'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$new_members = array($new_members);
|
||||
$new_members = (array)$new_members;
|
||||
}
|
||||
foreach($new_members as &$uid)
|
||||
{
|
||||
$uid = substr($uid,9); // cut off "urn:uuid:" prefix
|
||||
if (substr($uid, 0, 9) === 'urn:uuid:')
|
||||
{
|
||||
$uid = substr($uid,9); // cut off "urn:uuid:" prefix
|
||||
}
|
||||
}
|
||||
if ($oldContact)
|
||||
{
|
||||
@ -861,7 +896,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
// reread as update of list-members updates etag and modified
|
||||
if (($contact = $this->bo->read_list($list_id)))
|
||||
{
|
||||
// re-read group to get correct etag (not dublicate etag code here)
|
||||
// re-read group to get correct etag (not duplicate etag code here)
|
||||
$contact = $this->read($contact['list_'.self::$path_attr]);
|
||||
}
|
||||
}
|
||||
@ -1059,7 +1094,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
$keys = ['tid' => $non_deleted_tids];
|
||||
|
||||
// with REST/JSON we only use our id, but DELETE request has neither Accept nor Content-Type header to detect JSON request
|
||||
if (preg_match('/^(list-)?(\d+)$/', $id, $matches))
|
||||
if (preg_match('/^('.self::JS_CARDGROUP_ID_PREFIX.')?(\d+)$/', $id, $matches))
|
||||
{
|
||||
if (!empty($matches[1]))
|
||||
{
|
||||
|
@ -996,9 +996,10 @@ class CalDAV extends HTTP_WebDAV_Server
|
||||
/**
|
||||
* Check if client want or sends JSON
|
||||
*
|
||||
* @param string &$type=null
|
||||
* @return bool|string false: no json, true: application/json, string: application/(string)+json
|
||||
*/
|
||||
public static function isJSON(string $type=null)
|
||||
public static function isJSON(string &$type=null)
|
||||
{
|
||||
if (!isset($type))
|
||||
{
|
||||
|
@ -570,8 +570,9 @@ abstract class Handler
|
||||
* @param int|string $retval
|
||||
* @param boolean $path_attr_is_name =true true: path_attr is ca(l|rd)dav_name, false: id (GroupDAV needs Location header)
|
||||
* @param string $etag =null etag, to not calculate it again (if != null)
|
||||
* @param string $prefix='' prefix for id
|
||||
*/
|
||||
function put_response_headers($entry, $path, $retval, $path_attr_is_name=true, $etag=null)
|
||||
function put_response_headers($entry, $path, $retval, $path_attr_is_name=true, $etag=null, string $prefix='')
|
||||
{
|
||||
//error_log(__METHOD__."(".array2string($entry).", '$path', ".array2string($retval).", path_attr_is_name=$path_attr_is_name, etag=".array2string($etag).")");
|
||||
// we should not return an etag here, as EGroupware never stores ical/vcard byte-by-byte
|
||||
@ -589,9 +590,9 @@ abstract class Handler
|
||||
// send Location header only on success AND if we dont use caldav_name as path-attribute or
|
||||
if ((is_bool($retval) ? $retval : $retval[0] === '2') && (!$path_attr_is_name ||
|
||||
// POST with add-member query parameter
|
||||
$_SERVER['REQUEST_METHOD'] == 'POST' && isset($_GET['add-member'])) ||
|
||||
// in case we choose to use a different name for the resourece, give the client a hint
|
||||
basename($path) !== $this->new_id)
|
||||
$_SERVER['REQUEST_METHOD'] == 'POST') ||
|
||||
// in case we choose to use a different name for the resource, give the client a hint
|
||||
basename($path) !== $prefix.$this->new_id)
|
||||
{
|
||||
$path = preg_replace('|(.*)/[^/]*|', '\1/', $path);
|
||||
header('Location: '.$this->base_uri.$path.$this->new_id);
|
||||
|
@ -41,14 +41,14 @@ class JsContact
|
||||
throw new Api\Exception\NotFound();
|
||||
}
|
||||
$data = array_filter([
|
||||
'uid' => $contact['uid'],
|
||||
'uid' => self::uid($contact['uid']),
|
||||
'prodId' => 'EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['api']['version'],
|
||||
'created' => self::UTCDateTime($contact['created']),
|
||||
'updated' => self::UTCDateTime($contact['modified']),
|
||||
//'kind' => '', // 'individual' or 'org'
|
||||
//'relatedTo' => [],
|
||||
'name' => self::nameComponents($contact),
|
||||
'fullName' => self::localizedString($contact['n_fn']),
|
||||
'fullName' => $contact['n_fn'],
|
||||
//'nickNames' => [],
|
||||
'organizations' => array_filter(['org' => self::organization($contact)]),
|
||||
'titles' => self::titles($contact),
|
||||
@ -74,11 +74,11 @@ class JsContact
|
||||
]),
|
||||
'photos' => self::photos($contact),
|
||||
'anniversaries' => self::anniversaries($contact),
|
||||
'notes' => empty($contact['note']) ? null : [self::localizedString($contact['note'])],
|
||||
'notes' => empty($contact['note']) ? null : [$contact['note']],
|
||||
'categories' => self::categories($contact['cat_id']),
|
||||
'egroupware.org/customfields' => self::customfields($contact),
|
||||
'egroupware.org/assistant' => $contact['assistent'],
|
||||
'egroupware.org/fileAs' => $contact['fileas'],
|
||||
'egroupware.org:customfields' => self::customfields($contact),
|
||||
'egroupware.org:assistant' => $contact['assistent'],
|
||||
'egroupware.org:fileAs' => $contact['fileas'],
|
||||
]);
|
||||
if ($encode)
|
||||
{
|
||||
@ -119,7 +119,7 @@ class JsContact
|
||||
break;
|
||||
|
||||
case 'fullName':
|
||||
$contact['n_fn'] = self::parseLocalizedString($value);
|
||||
$contact['n_fn'] = self::parseString($value);
|
||||
break;
|
||||
|
||||
case 'organizations':
|
||||
@ -156,7 +156,7 @@ class JsContact
|
||||
|
||||
case 'notes':
|
||||
$contact['note'] = implode("\n", array_map(static function ($note) {
|
||||
return self::parseLocalizedString($note);
|
||||
return self::parseString($note);
|
||||
}, $value));
|
||||
break;
|
||||
|
||||
@ -164,15 +164,15 @@ class JsContact
|
||||
$contact['cat_id'] = self::parseCategories($value);
|
||||
break;
|
||||
|
||||
case 'egroupware.org/customfields':
|
||||
case 'egroupware.org:customfields':
|
||||
$contact += self::parseCustomfields($value);
|
||||
break;
|
||||
|
||||
case 'egroupware.org/assistant':
|
||||
case 'egroupware.org:assistant':
|
||||
$contact['assistent'] = $value;
|
||||
break;
|
||||
|
||||
case 'egroupware.org/fileAs':
|
||||
case 'egroupware.org:fileAs':
|
||||
$contact['fileas'] = $value;
|
||||
break;
|
||||
|
||||
@ -188,28 +188,47 @@ class JsContact
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\JsonException $e) {
|
||||
throw new JsContactParseException("Error parsing JSON: ".$e->getMessage(), 422, $e);
|
||||
}
|
||||
catch (\InvalidArgumentException $e) {
|
||||
throw new JsContactParseException("Error parsing JsContact field '$name': ".
|
||||
str_replace('"', "'", $e->getMessage()), 422);
|
||||
}
|
||||
catch (\TypeError $e) {
|
||||
$message = $e->getMessage();
|
||||
if (preg_match('/must be of the type ([^ ]+), ([^ ]+) given/', $message, $matches))
|
||||
{
|
||||
$message = "$matches[1] expected, but got $matches[2]: ".
|
||||
str_replace('"', "'", json_encode($value, self::JSON_OPTIONS_ERROR));
|
||||
}
|
||||
throw new JsContactParseException("Error parsing JsContact field '$name': $message", 422, $e);
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
throw new JsContactParseException("Error parsing JsContact field '$name': ". $e->getMessage(), 422, $e);
|
||||
self::handleExceptions($e, 'JsContact Card', $name, $value);
|
||||
}
|
||||
return $contact;
|
||||
}
|
||||
|
||||
const URN_UUID_PREFIX = 'urn:uuid:';
|
||||
const UUID_PREG = '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i';
|
||||
|
||||
/**
|
||||
* Get UID with either "urn:uuid:" prefix for UUIDs or just the text
|
||||
*
|
||||
* @param string $uid
|
||||
* @return string
|
||||
*/
|
||||
protected static function uid(string $uid)
|
||||
{
|
||||
return preg_match(self::UUID_PREG, $uid) ? self::URN_UUID_PREFIX.$uid : $uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and optionally generate UID
|
||||
*
|
||||
* @param string|null $uid
|
||||
* @param bool $generate_when_empty true: generate UID if empty, false: throw error
|
||||
* @return string without urn:uuid: prefix
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected static function parseUid(string $uid=null, $generate_when_empty=false)
|
||||
{
|
||||
if (empty($uid) || strlen($uid) < 12)
|
||||
{
|
||||
if (!$generate_when_empty)
|
||||
{
|
||||
throw new \InvalidArgumentException("Invalid or missing UID: ".json_encode($uid));
|
||||
}
|
||||
$uid = \HTTP_WebDAV_Server::_new_uuid();
|
||||
}
|
||||
return strpos($uid, self::URN_UUID_PREFIX) === 0 ? substr($uid, 9) : $uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON options for errors thrown as exceptions
|
||||
*/
|
||||
@ -229,8 +248,8 @@ class JsContact
|
||||
return null; // name is mandatory
|
||||
}
|
||||
return array_filter([
|
||||
'name' => self::localizedString($contact['org_name']),
|
||||
'units' => empty($contact['org_unit']) ? null : ['org_unit' => self::localizedString($contact['org_unit'])],
|
||||
'name' => $contact['org_name'],
|
||||
'units' => empty($contact['org_unit']) ? null : ['org_unit' => $contact['org_unit']],
|
||||
]);
|
||||
}
|
||||
|
||||
@ -247,10 +266,10 @@ class JsContact
|
||||
$contact = [];
|
||||
foreach($orgas as $orga)
|
||||
{
|
||||
$contact['org_name'] = self::parseLocalizedString($orga['name']);
|
||||
$contact['org_name'] = self::parseString($orga['name']);
|
||||
$contact['org_unit'] = implode(' ', array_map(static function($unit)
|
||||
{
|
||||
return self::parseLocalizedString($unit);
|
||||
return self::parseString($unit);
|
||||
}, $orga['units']));
|
||||
break;
|
||||
}
|
||||
@ -270,8 +289,8 @@ class JsContact
|
||||
protected static function titles(array $contact)
|
||||
{
|
||||
return array_filter([
|
||||
'title' => self::localizedString($contact['title']),
|
||||
'role' => self::localizedString($contact['role']),
|
||||
'title' => $contact['title'],
|
||||
'role' => $contact['role'],
|
||||
]);
|
||||
}
|
||||
|
||||
@ -286,21 +305,21 @@ class JsContact
|
||||
$contact = [];
|
||||
if (isset($titles[$id='title']) || isset($contact[$id='jobTitle']))
|
||||
{
|
||||
$contact['title'] = self::parseLocalizedString($titles[$id]);
|
||||
$contact['title'] = self::parseString($titles[$id]);
|
||||
unset($titles[$id]);
|
||||
}
|
||||
if (isset($titles[$id='role']))
|
||||
{
|
||||
$contact['role'] = self::parseLocalizedString($titles[$id]);
|
||||
$contact['role'] = self::parseString($titles[$id]);
|
||||
unset($titles[$id]);
|
||||
}
|
||||
if (!isset($contact['title']) && $titles)
|
||||
{
|
||||
$contact['title'] = self::parseLocalizedString(array_shift($titles));
|
||||
$contact['title'] = self::parseString(array_shift($titles));
|
||||
}
|
||||
if (!isset($contact['role']) && $titles)
|
||||
{
|
||||
$contact['role'] = self::parseLocalizedString(array_shift($titles));
|
||||
$contact['role'] = self::parseString(array_shift($titles));
|
||||
}
|
||||
if (count($titles))
|
||||
{
|
||||
@ -481,10 +500,10 @@ class JsContact
|
||||
'street' => self::streetComponents($contact[$prefix.'street'], $contact[$prefix.'street2']),
|
||||
]);
|
||||
// only add contexts and preference to non-empty address
|
||||
return !$address ? [] : $address+[
|
||||
return !$address ? [] : array_filter($address+[
|
||||
'contexts' => [$type => true],
|
||||
'pref' => $preference,
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -946,16 +965,12 @@ class JsContact
|
||||
*
|
||||
* We're not currently storing/allowing any localization --> they get ignored/thrown away!
|
||||
*
|
||||
* @param array $value object with attribute "value"
|
||||
* @param string $value =null
|
||||
* @return string
|
||||
*/
|
||||
protected static function parseLocalizedString(array $value)
|
||||
protected static function parseString(string $value=null)
|
||||
{
|
||||
if (!is_string($value['value']))
|
||||
{
|
||||
throw new \InvalidArgumentException("Invalid localizedString: ".json_encode($value, self::JSON_OPTIONS_ERROR));
|
||||
}
|
||||
return $value['value'];
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -996,10 +1011,10 @@ class JsContact
|
||||
throw new Api\Exception\NotFound();
|
||||
}
|
||||
$data = array_filter([
|
||||
'uid' => $group['list_uid'],
|
||||
'uid' => self::uid($group['list_uid']),
|
||||
'name' => $group['list_name'],
|
||||
'card' => self::getJsCard([
|
||||
'uid' => $group['list_uid'],
|
||||
'uid' => self::uid($group['list_uid']),
|
||||
'n_fn' => $group['list_name'], // --> fullName
|
||||
'modified' => $group['list_modified'], // no other way to send modification date
|
||||
], false),
|
||||
@ -1007,7 +1022,7 @@ class JsContact
|
||||
]);
|
||||
foreach($group['members'] as $uid)
|
||||
{
|
||||
$data['members'][$uid] = true;
|
||||
$data['members'][self::uid($uid)] = true;
|
||||
}
|
||||
if ($encode)
|
||||
{
|
||||
@ -1016,6 +1031,116 @@ class JsContact
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JsCard
|
||||
*
|
||||
* @param string $json
|
||||
* @return array
|
||||
*/
|
||||
public static function parseJsCardGroup(string $json)
|
||||
{
|
||||
try
|
||||
{
|
||||
$data = json_decode($json, true, 10, JSON_THROW_ON_ERROR);
|
||||
|
||||
if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist
|
||||
|
||||
// make sure missing mandatory members give an error
|
||||
$data += ['uid' => null, 'members' => null];
|
||||
$group = [];
|
||||
foreach ($data as $name => $value)
|
||||
{
|
||||
switch ($name)
|
||||
{
|
||||
case 'uid':
|
||||
$group['uid'] = self::parseUid($value);
|
||||
break;
|
||||
|
||||
case 'name':
|
||||
$group['n_fn'] = $value;
|
||||
break;
|
||||
|
||||
case 'card':
|
||||
$card = self::parseJsCard(json_encode($value, self::JSON_OPTIONS_ERROR));
|
||||
// prefer name over card-fullName
|
||||
if (!empty($card['n_fn']) && empty($group['n_fn']))
|
||||
{
|
||||
$group['n_fn'] = $card['n_fn'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'members':
|
||||
$group['members'] = self::parseMembers($value);
|
||||
break;
|
||||
|
||||
default:
|
||||
error_log(__METHOD__ . "() $name=" . json_encode($value, self::JSON_OPTIONS_ERROR) . ' --> ignored');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
self::handleExceptions($e, 'JsContact CardGroup', $name, $value);
|
||||
}
|
||||
return $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse members object
|
||||
*
|
||||
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-3.1.2
|
||||
* @param array $values uid => true pairs
|
||||
* @return array of uid's
|
||||
*/
|
||||
protected static function parseMembers(array $values)
|
||||
{
|
||||
$members = [];
|
||||
foreach($values as $uid => $value)
|
||||
{
|
||||
if (!is_string($uid) || $value !== true)
|
||||
{
|
||||
throw new \InvalidArgumentException('Invalid members object: '.json_encode($values, self::JSON_OPTIONS_ERROR));
|
||||
}
|
||||
$members[] = self::parseUid($uid);
|
||||
}
|
||||
return $members;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map all kind of exceptions while parsing to a JsContactParseException
|
||||
*
|
||||
* @param \Throwable $e
|
||||
* @param string $type
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @throws JsContactParseException
|
||||
*/
|
||||
protected static function handleExceptions(\Throwable $e, $type='JsContact', string $name, $value)
|
||||
{
|
||||
try {
|
||||
throw $e;
|
||||
}
|
||||
catch (\JsonException $e) {
|
||||
throw new JsContactParseException("Error parsing JSON: ".$e->getMessage(), 422, $e);
|
||||
}
|
||||
catch (\InvalidArgumentException $e) {
|
||||
throw new JsContactParseException("Error parsing $type attribute '$name': ".
|
||||
str_replace('"', "'", $e->getMessage()), 422);
|
||||
}
|
||||
catch (\TypeError $e) {
|
||||
$message = $e->getMessage();
|
||||
if (preg_match('/must be of the type ([^ ]+( or [^ ]+)*), ([^ ]+) given/', $message, $matches))
|
||||
{
|
||||
$message = "$matches[1] expected, but got $matches[3]: ".
|
||||
str_replace('"', "'", json_encode($value, self::JSON_OPTIONS_ERROR));
|
||||
}
|
||||
throw new JsContactParseException("Error parsing $type attribute '$name': $message", 422, $e);
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
throw new JsContactParseException("Error parsing $type attribute '$name': ". $e->getMessage(), 422, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Api\Contacts
|
||||
*/
|
||||
|
@ -41,7 +41,8 @@ from the data of a allprop PROPFIND, allow browsing CalDAV/CardDAV tree with a r
|
||||
> currently implemented only for contacts!
|
||||
|
||||
Following RFCs / drafts used/planned for JSON encoding of ressources
|
||||
* [draft-ietf-jmap-jscontact: JSContact: A JSON Representation of Contact Data](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07)
|
||||
* [draft-ietf-jmap-jscontact: JSContact: A JSON Representation of Contact Data](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07) ([*](#implemented-changes-to-jscontact-draft-07-from-next-draft))
|
||||
* [draft-ietf-jmap-jscontact-vcard-06: 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
|
||||
@ -66,9 +67,9 @@ curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Acc
|
||||
"fullName": { "value": "Default Tester" },
|
||||
"organizations": {
|
||||
"org": {
|
||||
"name": { "value": "default.org" },
|
||||
"name": "default.org",
|
||||
"units": {
|
||||
"org_unit": { "value": "department.default.org" }
|
||||
"org_unit": "department.default.org"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -83,7 +84,7 @@ curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Acc
|
||||
"url": { "resource": "https://www.test.com/", "type": "uri", "contexts": { "work": true } }
|
||||
},
|
||||
"notes": [
|
||||
{ "value": "Test test TEST\n\\server\\share\n\\\nother\nblah" }
|
||||
"Test test TEST\n\\server\\share\n\\\nother\nblah"
|
||||
],
|
||||
},
|
||||
"/<username>/addressbook/list-36": {
|
||||
@ -96,7 +97,7 @@ curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Acc
|
||||
"fullName": { "value": "Example distribution list" }
|
||||
},
|
||||
"members": {
|
||||
"5638-8623c4830472a8ede9f9f8b30d435ea4": true
|
||||
"urn:uuid:5638-8623c4830472a8ede9f9f8b30d435ea4": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -194,7 +195,7 @@ curl 'https://example.org/egroupware/groupdav.php/addressbook/?sync-token=https:
|
||||
{ "type": "personal", "value": "Default" },
|
||||
{ "type": "surname", "value": "Tester" }
|
||||
],
|
||||
"fullName": { "value": "Default Tester" },
|
||||
"fullName": "Default Tester",
|
||||
....
|
||||
}
|
||||
},
|
||||
@ -218,7 +219,7 @@ curl 'https://example.org/egroupware/groupdav.php/addressbook/5593' -H "Accept:
|
||||
{ "type": "personal", "value": "Default" },
|
||||
{ "type": "surname", "value": "Tester" }
|
||||
],
|
||||
"fullName": { "value": "Default Tester" },
|
||||
"fullName": "Default Tester",
|
||||
....
|
||||
}
|
||||
```
|
||||
@ -255,3 +256,7 @@ Location: https://example.org/egroupware/groupdav.php/<username>/addressbook/123
|
||||
* **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 to [JsContact draft 07](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07) from next draft:
|
||||
* 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
|
Loading…
Reference in New Issue
Block a user