mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-26 16:48:49 +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;
|
var $home_set_pref;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for JsCardGroup id
|
||||||
|
*/
|
||||||
|
const JS_CARDGROUP_ID_PREFIX = 'list-';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
@ -350,7 +355,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
|||||||
{
|
{
|
||||||
foreach($lists as $list)
|
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'];
|
$etag = $list['list_id'].':'.$list['list_etag'];
|
||||||
// for all-in-one addressbook, add selected ABs to etag
|
// for all-in-one addressbook, add selected ABs to etag
|
||||||
if (isset($filter['owner']) && is_array($filter['owner']))
|
if (isset($filter['owner']) && is_array($filter['owner']))
|
||||||
@ -637,9 +642,33 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
|||||||
return $oldContact;
|
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
|
/* uncomment to return parsed data for testing
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
@ -687,7 +716,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
|||||||
$contactId = -1;
|
$contactId = -1;
|
||||||
$retval = '201 Created';
|
$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']))
|
if ($oldContact && $is_group !== isset($oldContact['list_id']))
|
||||||
{
|
{
|
||||||
throw new Api\Exception\AssertionFailed(__METHOD__."(,'$id',$user,'$prefix') can contact into group or visa-versa!");
|
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);
|
if ($this->http_if_match) $contact['etag'] = self::etag2value($this->http_if_match);
|
||||||
|
|
||||||
// ignore photo for JSON/REST, it's not yet supported
|
// 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 (!($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");
|
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)))
|
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']);
|
$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, ...
|
// 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));
|
if ($this->debug > 1) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($retval));
|
||||||
return $retval;
|
return $retval;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save distribition-list / group
|
* Save distribution-list / group
|
||||||
*
|
*
|
||||||
* @param array $contact
|
* @param array $contact
|
||||||
* @param array|false $oldContact
|
* @param array|false $oldContact
|
||||||
@ -813,18 +845,21 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
|||||||
$contact['owner'], null, $data)))
|
$contact['owner'], null, $data)))
|
||||||
{
|
{
|
||||||
// update members given in $contact['##X-ADDRESSBOOKSERVER-MEMBER']
|
// update members given in $contact['##X-ADDRESSBOOKSERVER-MEMBER']
|
||||||
$new_members = $contact['##X-ADDRESSBOOKSERVER-MEMBER'];
|
$new_members = $contact['members'] ?: $contact['##X-ADDRESSBOOKSERVER-MEMBER'];
|
||||||
if ($new_members[1] == ':' && ($n = unserialize($new_members)))
|
if (is_string($new_members) && $new_members[1] === ':' && ($n = unserialize($new_members)))
|
||||||
{
|
{
|
||||||
$new_members = $n['values'];
|
$new_members = $n['values'];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$new_members = array($new_members);
|
$new_members = (array)$new_members;
|
||||||
}
|
}
|
||||||
foreach($new_members as &$uid)
|
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)
|
if ($oldContact)
|
||||||
{
|
{
|
||||||
@ -861,7 +896,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
|||||||
// reread as update of list-members updates etag and modified
|
// reread as update of list-members updates etag and modified
|
||||||
if (($contact = $this->bo->read_list($list_id)))
|
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]);
|
$contact = $this->read($contact['list_'.self::$path_attr]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1059,7 +1094,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
|||||||
$keys = ['tid' => $non_deleted_tids];
|
$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
|
// 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]))
|
if (!empty($matches[1]))
|
||||||
{
|
{
|
||||||
|
@ -996,9 +996,10 @@ class CalDAV extends HTTP_WebDAV_Server
|
|||||||
/**
|
/**
|
||||||
* Check if client want or sends JSON
|
* Check if client want or sends JSON
|
||||||
*
|
*
|
||||||
|
* @param string &$type=null
|
||||||
* @return bool|string false: no json, true: application/json, string: application/(string)+json
|
* @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))
|
if (!isset($type))
|
||||||
{
|
{
|
||||||
|
@ -570,8 +570,9 @@ abstract class Handler
|
|||||||
* @param int|string $retval
|
* @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 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 $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).")");
|
//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
|
// 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
|
// 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 ||
|
if ((is_bool($retval) ? $retval : $retval[0] === '2') && (!$path_attr_is_name ||
|
||||||
// POST with add-member query parameter
|
// POST with add-member query parameter
|
||||||
$_SERVER['REQUEST_METHOD'] == 'POST' && isset($_GET['add-member'])) ||
|
$_SERVER['REQUEST_METHOD'] == 'POST') ||
|
||||||
// in case we choose to use a different name for the resourece, give the client a hint
|
// in case we choose to use a different name for the resource, give the client a hint
|
||||||
basename($path) !== $this->new_id)
|
basename($path) !== $prefix.$this->new_id)
|
||||||
{
|
{
|
||||||
$path = preg_replace('|(.*)/[^/]*|', '\1/', $path);
|
$path = preg_replace('|(.*)/[^/]*|', '\1/', $path);
|
||||||
header('Location: '.$this->base_uri.$path.$this->new_id);
|
header('Location: '.$this->base_uri.$path.$this->new_id);
|
||||||
|
@ -41,14 +41,14 @@ class JsContact
|
|||||||
throw new Api\Exception\NotFound();
|
throw new Api\Exception\NotFound();
|
||||||
}
|
}
|
||||||
$data = array_filter([
|
$data = array_filter([
|
||||||
'uid' => $contact['uid'],
|
'uid' => self::uid($contact['uid']),
|
||||||
'prodId' => 'EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['api']['version'],
|
'prodId' => 'EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['api']['version'],
|
||||||
'created' => self::UTCDateTime($contact['created']),
|
'created' => self::UTCDateTime($contact['created']),
|
||||||
'updated' => self::UTCDateTime($contact['modified']),
|
'updated' => self::UTCDateTime($contact['modified']),
|
||||||
//'kind' => '', // 'individual' or 'org'
|
//'kind' => '', // 'individual' or 'org'
|
||||||
//'relatedTo' => [],
|
//'relatedTo' => [],
|
||||||
'name' => self::nameComponents($contact),
|
'name' => self::nameComponents($contact),
|
||||||
'fullName' => self::localizedString($contact['n_fn']),
|
'fullName' => $contact['n_fn'],
|
||||||
//'nickNames' => [],
|
//'nickNames' => [],
|
||||||
'organizations' => array_filter(['org' => self::organization($contact)]),
|
'organizations' => array_filter(['org' => self::organization($contact)]),
|
||||||
'titles' => self::titles($contact),
|
'titles' => self::titles($contact),
|
||||||
@ -74,11 +74,11 @@ class JsContact
|
|||||||
]),
|
]),
|
||||||
'photos' => self::photos($contact),
|
'photos' => self::photos($contact),
|
||||||
'anniversaries' => self::anniversaries($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']),
|
'categories' => self::categories($contact['cat_id']),
|
||||||
'egroupware.org/customfields' => self::customfields($contact),
|
'egroupware.org:customfields' => self::customfields($contact),
|
||||||
'egroupware.org/assistant' => $contact['assistent'],
|
'egroupware.org:assistant' => $contact['assistent'],
|
||||||
'egroupware.org/fileAs' => $contact['fileas'],
|
'egroupware.org:fileAs' => $contact['fileas'],
|
||||||
]);
|
]);
|
||||||
if ($encode)
|
if ($encode)
|
||||||
{
|
{
|
||||||
@ -119,7 +119,7 @@ class JsContact
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fullName':
|
case 'fullName':
|
||||||
$contact['n_fn'] = self::parseLocalizedString($value);
|
$contact['n_fn'] = self::parseString($value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'organizations':
|
case 'organizations':
|
||||||
@ -156,7 +156,7 @@ class JsContact
|
|||||||
|
|
||||||
case 'notes':
|
case 'notes':
|
||||||
$contact['note'] = implode("\n", array_map(static function ($note) {
|
$contact['note'] = implode("\n", array_map(static function ($note) {
|
||||||
return self::parseLocalizedString($note);
|
return self::parseString($note);
|
||||||
}, $value));
|
}, $value));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -164,15 +164,15 @@ class JsContact
|
|||||||
$contact['cat_id'] = self::parseCategories($value);
|
$contact['cat_id'] = self::parseCategories($value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'egroupware.org/customfields':
|
case 'egroupware.org:customfields':
|
||||||
$contact += self::parseCustomfields($value);
|
$contact += self::parseCustomfields($value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'egroupware.org/assistant':
|
case 'egroupware.org:assistant':
|
||||||
$contact['assistent'] = $value;
|
$contact['assistent'] = $value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'egroupware.org/fileAs':
|
case 'egroupware.org:fileAs':
|
||||||
$contact['fileas'] = $value;
|
$contact['fileas'] = $value;
|
||||||
break;
|
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) {
|
catch (\Throwable $e) {
|
||||||
throw new JsContactParseException("Error parsing JsContact field '$name': ". $e->getMessage(), 422, $e);
|
self::handleExceptions($e, 'JsContact Card', $name, $value);
|
||||||
}
|
}
|
||||||
return $contact;
|
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
|
* JSON options for errors thrown as exceptions
|
||||||
*/
|
*/
|
||||||
@ -229,8 +248,8 @@ class JsContact
|
|||||||
return null; // name is mandatory
|
return null; // name is mandatory
|
||||||
}
|
}
|
||||||
return array_filter([
|
return array_filter([
|
||||||
'name' => self::localizedString($contact['org_name']),
|
'name' => $contact['org_name'],
|
||||||
'units' => empty($contact['org_unit']) ? null : ['org_unit' => self::localizedString($contact['org_unit'])],
|
'units' => empty($contact['org_unit']) ? null : ['org_unit' => $contact['org_unit']],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,10 +266,10 @@ class JsContact
|
|||||||
$contact = [];
|
$contact = [];
|
||||||
foreach($orgas as $orga)
|
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)
|
$contact['org_unit'] = implode(' ', array_map(static function($unit)
|
||||||
{
|
{
|
||||||
return self::parseLocalizedString($unit);
|
return self::parseString($unit);
|
||||||
}, $orga['units']));
|
}, $orga['units']));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -270,8 +289,8 @@ class JsContact
|
|||||||
protected static function titles(array $contact)
|
protected static function titles(array $contact)
|
||||||
{
|
{
|
||||||
return array_filter([
|
return array_filter([
|
||||||
'title' => self::localizedString($contact['title']),
|
'title' => $contact['title'],
|
||||||
'role' => self::localizedString($contact['role']),
|
'role' => $contact['role'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,21 +305,21 @@ class JsContact
|
|||||||
$contact = [];
|
$contact = [];
|
||||||
if (isset($titles[$id='title']) || isset($contact[$id='jobTitle']))
|
if (isset($titles[$id='title']) || isset($contact[$id='jobTitle']))
|
||||||
{
|
{
|
||||||
$contact['title'] = self::parseLocalizedString($titles[$id]);
|
$contact['title'] = self::parseString($titles[$id]);
|
||||||
unset($titles[$id]);
|
unset($titles[$id]);
|
||||||
}
|
}
|
||||||
if (isset($titles[$id='role']))
|
if (isset($titles[$id='role']))
|
||||||
{
|
{
|
||||||
$contact['role'] = self::parseLocalizedString($titles[$id]);
|
$contact['role'] = self::parseString($titles[$id]);
|
||||||
unset($titles[$id]);
|
unset($titles[$id]);
|
||||||
}
|
}
|
||||||
if (!isset($contact['title']) && $titles)
|
if (!isset($contact['title']) && $titles)
|
||||||
{
|
{
|
||||||
$contact['title'] = self::parseLocalizedString(array_shift($titles));
|
$contact['title'] = self::parseString(array_shift($titles));
|
||||||
}
|
}
|
||||||
if (!isset($contact['role']) && $titles)
|
if (!isset($contact['role']) && $titles)
|
||||||
{
|
{
|
||||||
$contact['role'] = self::parseLocalizedString(array_shift($titles));
|
$contact['role'] = self::parseString(array_shift($titles));
|
||||||
}
|
}
|
||||||
if (count($titles))
|
if (count($titles))
|
||||||
{
|
{
|
||||||
@ -481,10 +500,10 @@ class JsContact
|
|||||||
'street' => self::streetComponents($contact[$prefix.'street'], $contact[$prefix.'street2']),
|
'street' => self::streetComponents($contact[$prefix.'street'], $contact[$prefix.'street2']),
|
||||||
]);
|
]);
|
||||||
// only add contexts and preference to non-empty address
|
// only add contexts and preference to non-empty address
|
||||||
return !$address ? [] : $address+[
|
return !$address ? [] : array_filter($address+[
|
||||||
'contexts' => [$type => true],
|
'contexts' => [$type => true],
|
||||||
'pref' => $preference,
|
'pref' => $preference,
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -946,16 +965,12 @@ class JsContact
|
|||||||
*
|
*
|
||||||
* We're not currently storing/allowing any localization --> they get ignored/thrown away!
|
* 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
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected static function parseLocalizedString(array $value)
|
protected static function parseString(string $value=null)
|
||||||
{
|
{
|
||||||
if (!is_string($value['value']))
|
return $value;
|
||||||
{
|
|
||||||
throw new \InvalidArgumentException("Invalid localizedString: ".json_encode($value, self::JSON_OPTIONS_ERROR));
|
|
||||||
}
|
|
||||||
return $value['value'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -996,10 +1011,10 @@ class JsContact
|
|||||||
throw new Api\Exception\NotFound();
|
throw new Api\Exception\NotFound();
|
||||||
}
|
}
|
||||||
$data = array_filter([
|
$data = array_filter([
|
||||||
'uid' => $group['list_uid'],
|
'uid' => self::uid($group['list_uid']),
|
||||||
'name' => $group['list_name'],
|
'name' => $group['list_name'],
|
||||||
'card' => self::getJsCard([
|
'card' => self::getJsCard([
|
||||||
'uid' => $group['list_uid'],
|
'uid' => self::uid($group['list_uid']),
|
||||||
'n_fn' => $group['list_name'], // --> fullName
|
'n_fn' => $group['list_name'], // --> fullName
|
||||||
'modified' => $group['list_modified'], // no other way to send modification date
|
'modified' => $group['list_modified'], // no other way to send modification date
|
||||||
], false),
|
], false),
|
||||||
@ -1007,7 +1022,7 @@ class JsContact
|
|||||||
]);
|
]);
|
||||||
foreach($group['members'] as $uid)
|
foreach($group['members'] as $uid)
|
||||||
{
|
{
|
||||||
$data['members'][$uid] = true;
|
$data['members'][self::uid($uid)] = true;
|
||||||
}
|
}
|
||||||
if ($encode)
|
if ($encode)
|
||||||
{
|
{
|
||||||
@ -1016,6 +1031,116 @@ class JsContact
|
|||||||
return $data;
|
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
|
* @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!
|
> currently implemented only for contacts!
|
||||||
|
|
||||||
Following RFCs / drafts used/planned for JSON encoding of ressources
|
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)
|
* [rfc8984: JSCalendar: A JSON Representation of Calendar Data](https://datatracker.ietf.org/doc/html/rfc8984)
|
||||||
|
|
||||||
### Supported request methods and examples
|
### Supported request methods and examples
|
||||||
@ -66,9 +67,9 @@ curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Acc
|
|||||||
"fullName": { "value": "Default Tester" },
|
"fullName": { "value": "Default Tester" },
|
||||||
"organizations": {
|
"organizations": {
|
||||||
"org": {
|
"org": {
|
||||||
"name": { "value": "default.org" },
|
"name": "default.org",
|
||||||
"units": {
|
"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 } }
|
"url": { "resource": "https://www.test.com/", "type": "uri", "contexts": { "work": true } }
|
||||||
},
|
},
|
||||||
"notes": [
|
"notes": [
|
||||||
{ "value": "Test test TEST\n\\server\\share\n\\\nother\nblah" }
|
"Test test TEST\n\\server\\share\n\\\nother\nblah"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"/<username>/addressbook/list-36": {
|
"/<username>/addressbook/list-36": {
|
||||||
@ -96,7 +97,7 @@ curl https://example.org/egroupware/groupdav.php/<username>/addressbook/ -H "Acc
|
|||||||
"fullName": { "value": "Example distribution list" }
|
"fullName": { "value": "Example distribution list" }
|
||||||
},
|
},
|
||||||
"members": {
|
"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": "personal", "value": "Default" },
|
||||||
{ "type": "surname", "value": "Tester" }
|
{ "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": "personal", "value": "Default" },
|
||||||
{ "type": "surname", "value": "Tester" }
|
{ "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
|
* **DELETE** requests delete single resources
|
||||||
|
|
||||||
* one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API
|
* 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