finished REST API for contacts modulo docu and bugs ;)

- JsCardGroup now used for distribution lists
- responses are not in "responses" attribute (no longer in root of object)
- fix sometimes empty / different members between PROPFIND/REPORT/JSON-GET and GET of group (caused by wrongly implemented limit to given AB)
- JSON pretty-print only if requested by Accept: application/pretty+json
- fix invalid JSON for errors (caused by opening {"responses": already sent
This commit is contained in:
Ralf Becker 2021-09-19 11:09:44 +02:00
parent 3bc015a90d
commit ce5389d0d5
6 changed files with 79 additions and 62 deletions

View File

@ -350,7 +350,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
{ {
foreach($lists as $list) foreach($lists as $list)
{ {
$list[self::$path_attr] = $list['list_carddav_name']; $list[self::$path_attr] = $is_jscontact ? 'list-'.$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']))
@ -365,7 +365,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
); );
if ($address_data) if ($address_data)
{ {
$content = $is_jscontact ? JsContact::getJsCardGroup($list) : $handler->getGroupVCard($list); $content = $is_jscontact ? JsContact::getJsCardGroup($list, false) : $handler->getGroupVCard($list);
$props['getcontentlength'] = bytes($content); $props['getcontentlength'] = bytes($content);
$props['address-data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content); $props['address-data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
} }
@ -597,10 +597,10 @@ class addressbook_groupdav extends Api\CalDAV\Handler
return $contact; return $contact;
} }
// jsContact or vCard // jsContact or vCard
if (Api\CalDAV::isJSON()) if (($type=Api\CalDAV::isJSON()))
{ {
$options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact) : $options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact, $type) :
JsContact::getJsCard($contact); JsContact::getJsCard($contact, $type);
$options['mimetype'] = ($contact['list_id'] ? JsContact::MIME_TYPE_JSCARDGROUP : $options['mimetype'] = ($contact['list_id'] ? JsContact::MIME_TYPE_JSCARDGROUP :
JsContact::MIME_TYPE_JSCARD).';charset=utf-8'; JsContact::MIME_TYPE_JSCARD).';charset=utf-8';
} }
@ -1057,16 +1057,24 @@ class addressbook_groupdav extends Api\CalDAV\Handler
$non_deleted_tids = array_keys($tids); $non_deleted_tids = array_keys($tids);
} }
$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 ((string)$id === (string)(int)$id) if (preg_match('/^(list-)?(\d+)$/', $id, $matches))
{ {
$keys['id'] = $id; if (!empty($matches[1]))
{
$keys = ['list_id' => $matches[2]];
}
else
{
$keys['id'] = $id;
}
} }
else else
{ {
$keys[self::$path_attr] = $id; $keys[self::$path_attr] = $id;
} }
$contact = $this->bo->read($keys); $contact = isset($keys['list_id']) ? false: $this->bo->read($keys);
// if contact not found and accounts stored NOT like contacts, try reading it without path-extension as id // if contact not found and accounts stored NOT like contacts, try reading it without path-extension as id
if (is_null($contact) && $this->bo->so_accounts && ($c = $this->bo->read($test=basename($id, '.vcf')))) if (is_null($contact) && $this->bo->so_accounts && ($c = $this->bo->read($test=basename($id, '.vcf'))))
@ -1086,12 +1094,13 @@ class addressbook_groupdav extends Api\CalDAV\Handler
$limit_in_ab[] = $GLOBALS['egw_info']['user']['account_id']; $limit_in_ab[] = $GLOBALS['egw_info']['user']['account_id'];
} }
/* we are currently not syncing distribution-lists/groups to /addressbook/ as /* we are currently not syncing distribution-lists/groups to /addressbook/ as
* Apple clients use that only as directory gateway * Apple clients use that only as directory gateway*/
elseif ($account_lid == 'addressbook') // /addressbook/ contains all readably contacts elseif (Api\CalDAV::isJSON() && $account_lid == 'addressbook') // /addressbook/ contains all readably contacts
{ {
$limit_in_ab = array_keys($this->bo->grants); $limit_in_ab = array_keys($this->bo->grants);
}*/ }
if (!$contact && ($contact = $this->bo->read_lists(array('list_'.self::$path_attr => $id),'contact_uid',$limit_in_ab))) if (!$contact && ($contact = $this->bo->read_lists(isset($keys['list_id']) ? $keys :
['list_'.self::$path_attr => $id],'contact_uid',$limit_in_ab)))
{ {
$contact = array_shift($contact); $contact = array_shift($contact);
$contact['n_fn'] = $contact['n_family'] = $contact['list_name']; $contact['n_fn'] = $contact['n_family'] = $contact['list_name'];

View File

@ -994,9 +994,9 @@ class CalDAV extends HTTP_WebDAV_Server
} }
/** /**
* Check if clients want's or sends JSON * Check if client want or sends JSON
* *
* @return bool * @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)
{ {
@ -1005,7 +1005,8 @@ class CalDAV extends HTTP_WebDAV_Server
$type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST', 'PROPPATCH']) ? $type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST', 'PROPPATCH']) ?
$_SERVER['HTTP_CONTENT_TYPE'] : $_SERVER['HTTP_ACCEPT']; $_SERVER['HTTP_CONTENT_TYPE'] : $_SERVER['HTTP_ACCEPT'];
} }
return (bool)preg_match('#application/([^+ ;]+\+)?json#', $type); return preg_match('#application/(([^+ ;]+)\+)?json#', $type, $matches) ?
(empty($matches[1]) ? true : $matches[2]) : false;
} }
/** /**
@ -1021,9 +1022,9 @@ class CalDAV extends HTTP_WebDAV_Server
$id = $app = $user = null; $id = $app = $user = null;
if (!$this->_parse_path($options['path'],$id,$app,$user) || $app == 'principals') if (!$this->_parse_path($options['path'],$id,$app,$user) || $app == 'principals')
{ {
if (self::isJSON()) if (($json = self::isJSON()))
{ {
return $this->jsonIndex($options); return $this->jsonIndex($options, $json === 'pretty');
} }
return $this->autoindex($options); return $this->autoindex($options);
} }
@ -1048,7 +1049,7 @@ class CalDAV extends HTTP_WebDAV_Server
{ {
if (!$pretty) if (!$pretty)
{ {
return self::json_encode($data, self::JSON_OPTIONS); return json_encode($data, self::JSON_OPTIONS);
} }
return preg_replace('/: {\n\s*(.*?)\n\s*(},?\n)/', ': { $1 $2', return preg_replace('/: {\n\s*(.*?)\n\s*(},?\n)/', ': { $1 $2',
json_encode($data, self::JSON_OPTIONS_PRETTY)); json_encode($data, self::JSON_OPTIONS_PRETTY));
@ -1066,9 +1067,10 @@ class CalDAV extends HTTP_WebDAV_Server
* } * }
* *
* @param array $options * @param array $options
* @param bool $pretty =false true: pretty-print JSON
* @return bool|string|void * @return bool|string|void
*/ */
protected function jsonIndex(array $options) protected function jsonIndex(array $options, bool $pretty)
{ {
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
$is_addressbook = strpos($options['path'], '/addressbook') !== false; $is_addressbook = strpos($options['path'], '/addressbook') !== false;
@ -1118,9 +1120,8 @@ class CalDAV extends HTTP_WebDAV_Server
{ {
return $ret; // no collection return $ret; // no collection
} }
// set start as prefix, to no have it in front of exceptions
echo "{\n"; $prefix = "{\n\t\"responses\": {\n";
$prefix = " ";
foreach($files['files'] as $resource) foreach($files['files'] as $resource)
{ {
$path = $resource['path']; $path = $resource['path'];
@ -1129,10 +1130,6 @@ class CalDAV extends HTTP_WebDAV_Server
{ {
echo 'null'; // deleted in sync-report echo 'null'; // deleted in sync-report
} }
/*elseif (isset($resource['props']['address-data']))
{
echo $resource['props']['address-data']['val'];
}*/
else else
{ {
$props = $propfind_options['props'] === 'all' ? $resource['props'] : $props = $propfind_options['props'] === 'all' ? $resource['props'] :
@ -1146,17 +1143,24 @@ class CalDAV extends HTTP_WebDAV_Server
{ {
$props = current($props)['val']; $props = current($props)['val'];
} }
echo self::json_encode($props); echo self::json_encode($props, $pretty);
} }
$prefix = ",\n "; $prefix = ",\n";
} }
// add sync-token to response // happens with an empty response
if ($prefix !== ",\n")
{
echo $prefix;
$prefix = ",\n";
}
echo "\n\t}";
// add sync-token and more-results to response
if (isset($files['sync-token'])) if (isset($files['sync-token']))
{ {
echo $prefix.'"sync-token": '.json_encode(!is_callable($files['sync-token']) ? $files['sync-token'] : echo $prefix."\t".'"sync-token": '.json_encode(!is_callable($files['sync-token']) ? $files['sync-token'] :
call_user_func_array($files['sync-token'], (array)$files['sync-token-params']), JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); call_user_func_array($files['sync-token'], (array)$files['sync-token-params']), JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
} }
echo "\n}\n"; echo "\n}";
// exit now, so WebDAV::GET does NOT add Content-Type: application/octet-stream // exit now, so WebDAV::GET does NOT add Content-Type: application/octet-stream
exit; exit;

View File

@ -714,7 +714,7 @@ abstract class Handler
{ {
if (Api\CalDAV::isJSON()) if (Api\CalDAV::isJSON())
{ {
$error = ",\n".' "more-results": true'; $error = ",\n\t".'"more-results": true';
} }
else else
{ {

View File

@ -30,7 +30,7 @@ class JsContact
* Get jsCard for given contact * Get jsCard for given contact
* *
* @param int|array $contact * @param int|array $contact
* @param bool $encode=true true: JSON encode, false: return raw data eg. from listing * @param bool|"pretty" $encode=true true: JSON encode, "pretty": JSON encode with pretty-print, false: return raw data eg. from listing
* @return string|array * @return string|array
* @throws Api\Exception\NotFound * @throws Api\Exception\NotFound
*/ */
@ -68,13 +68,13 @@ class JsContact
'url' => !empty($contact['url']) ? ['resource' => $contact['url'], 'type' => 'uri', 'contexts' => ['work' => true]] : null, 'url' => !empty($contact['url']) ? ['resource' => $contact['url'], 'type' => 'uri', 'contexts' => ['work' => true]] : null,
'url_home' => !empty($contact['url_home']) ? ['resource' => $contact['url_home'], 'type' => 'uri', 'contexts' => ['private' => true]] : null, 'url_home' => !empty($contact['url_home']) ? ['resource' => $contact['url_home'], 'type' => 'uri', 'contexts' => ['private' => true]] : null,
]), ]),
'addresses' => [ 'addresses' => array_filter([
'work' => self::address($contact, 'work', 1), // as it's the more prominent in our UI 'work' => self::address($contact, 'work', 1), // as it's the more prominent in our UI
'home' => self::address($contact, 'home'), 'home' => self::address($contact, 'home'),
], ]),
'photos' => self::photos($contact), 'photos' => self::photos($contact),
'anniversaries' => self::anniversaries($contact), 'anniversaries' => self::anniversaries($contact),
'notes' => [self::localizedString($contact['note'])], 'notes' => empty($contact['note']) ? null : [self::localizedString($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'],
@ -82,7 +82,7 @@ class JsContact
]); ]);
if ($encode) if ($encode)
{ {
return Api\CalDAV::json_encode($data, self::JSON_OPTIONS_ERROR); return Api\CalDAV::json_encode($data, $encode === "pretty");
} }
return $data; return $data;
} }
@ -474,16 +474,17 @@ class JsContact
$js2attr = self::$jsAddress2attr; $js2attr = self::$jsAddress2attr;
if ($type === 'work') $js2attr += self::$jsAddress2workAttr; if ($type === 'work') $js2attr += self::$jsAddress2workAttr;
$address = array_map(static function($attr) use ($contact, $prefix) $address = array_filter(array_map(static function($attr) use ($contact, $prefix)
{ {
return $contact[$prefix.$attr]; return $contact[$prefix.$attr];
}, $js2attr) + [ }, $js2attr) + [
'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
return !$address ? [] : $address+[
'contexts' => [$type => true], 'contexts' => [$type => true],
'pref' => $preference, 'pref' => $preference,
]; ];
return array_filter($address);
} }
/** /**
@ -984,33 +985,35 @@ class JsContact
* Get jsCardGroup for given group * Get jsCardGroup for given group
* *
* @param int|array $group * @param int|array $group
* @return string * @param bool|"pretty" $encode=true true: JSON, "pretty": JSON pretty-print, false: array
* @return array|string
* @throws Api\Exception\NotFound * @throws Api\Exception\NotFound
*/ */
public static function getJsCardGroup($group) public static function getJsCardGroup($group, $encode=true)
{ {
if (is_scalar($group) && !($group = self::getContacts()->read_lists($group))) if (is_scalar($group) && !($group = self::getContacts()->read_lists($group)))
{ {
throw new Api\Exception\NotFound(); throw new Api\Exception\NotFound();
} }
/* $data = array_filter([
$vCard = new Horde_Icalendar_Vcard($version); 'uid' => $group['list_uid'],
$vCard->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['api']['version'].'//'. 'name' => $group['list_name'],
strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang'])); 'card' => self::getJsCard([
'uid' => $group['list_uid'],
$vCard->setAttribute('N',$list['list_name'],array(),true,array($list['list_name'],'','','','')); 'n_fn' => $group['list_name'], // --> fullName
$vCard->setAttribute('FN',$list['list_name']); 'modified' => $group['list_modified'], // no other way to send modification date
], false),
$vCard->setAttribute('X-ADDRESSBOOKSERVER-KIND','group'); 'members' => [],
foreach($list['members'] as $uid) ]);
foreach($group['members'] as $uid)
{ {
$vCard->setAttribute('X-ADDRESSBOOKSERVER-MEMBER','urn:uuid:'.$uid); $data['members'][$uid] = true;
} }
$vCard->setAttribute('REV',Api\DateTime::to($list['list_modified'],'Y-m-d\TH:i:s\Z')); if ($encode)
$vCard->setAttribute('UID',$list['list_uid']); {
*/ $data = Api\CalDAV::json_encode($data, $encode === 'pretty');
}
return Api\CalDAV::json_encode($group, self::JSON_OPTIONS_ERROR); return $data;
} }
/** /**

View File

@ -810,14 +810,14 @@ class Sql extends Api\Storage
{ {
if ($limit_in_ab) if ($limit_in_ab)
{ {
$in_ab_join = " JOIN $this->lists_table ON $this->lists_table.list_id=$this->ab2list_table.list_id AND $this->lists_table."; $in_ab_join = " JOIN $this->lists_table ON $this->lists_table.list_id=$this->ab2list_table.list_id AND ";
if (!is_bool($limit_in_ab)) if (!is_bool($limit_in_ab))
{ {
$in_ab_join .= $this->db->expression($this->lists_table, array('list_owner'=>$limit_in_ab)); $in_ab_join .= $this->db->expression($this->table_name, $this->table_name.'.', ['contact_owner' => $limit_in_ab]);
} }
else else
{ {
$in_ab_join .= "list_owner=$this->table_name.contact_owner"; $in_ab_join .= "$this->lists_table.list_owner=$this->table_name.contact_owner";
} }
} }
foreach($this->db->select($this->ab2list_table,"$this->ab2list_table.list_id,$this->table_name.$member_attr", foreach($this->db->select($this->ab2list_table,"$this->ab2list_table.list_id,$this->table_name.$member_attr",

View File

@ -1209,7 +1209,8 @@ class Storage
* *
* @param array $keys column-name => value(s) pairs, eg. array('list_uid'=>$uid) * @param array $keys column-name => value(s) pairs, eg. array('list_uid'=>$uid)
* @param string $member_attr ='contact_uid' null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute * @param string $member_attr ='contact_uid' null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute
* @param boolean $limit_in_ab =false if true only return members from the same owners addressbook * @param boolean|int|array $limit_in_ab =false if true only return members from the same owners addressbook,
* if int|array only return members from the given owners addressbook(s)
* @return array with list_id => array(list_id,list_name,list_owner,...) pairs * @return array with list_id => array(list_id,list_name,list_owner,...) pairs
*/ */
function read_lists($keys,$member_attr=null,$limit_in_ab=false) function read_lists($keys,$member_attr=null,$limit_in_ab=false)