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)
{
$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'];
// for all-in-one addressbook, add selected ABs to etag
if (isset($filter['owner']) && is_array($filter['owner']))
@ -365,7 +365,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
);
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['address-data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
}
@ -597,10 +597,10 @@ class addressbook_groupdav extends Api\CalDAV\Handler
return $contact;
}
// jsContact or vCard
if (Api\CalDAV::isJSON())
if (($type=Api\CalDAV::isJSON()))
{
$options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact) :
JsContact::getJsCard($contact);
$options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact, $type) :
JsContact::getJsCard($contact, $type);
$options['mimetype'] = ($contact['list_id'] ? JsContact::MIME_TYPE_JSCARDGROUP :
JsContact::MIME_TYPE_JSCARD).';charset=utf-8';
}
@ -1057,16 +1057,24 @@ class addressbook_groupdav extends Api\CalDAV\Handler
$non_deleted_tids = array_keys($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
if ((string)$id === (string)(int)$id)
if (preg_match('/^(list-)?(\d+)$/', $id, $matches))
{
if (!empty($matches[1]))
{
$keys = ['list_id' => $matches[2]];
}
else
{
$keys['id'] = $id;
}
}
else
{
$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 (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'];
}
/* we are currently not syncing distribution-lists/groups to /addressbook/ as
* Apple clients use that only as directory gateway
elseif ($account_lid == 'addressbook') // /addressbook/ contains all readably contacts
* Apple clients use that only as directory gateway*/
elseif (Api\CalDAV::isJSON() && $account_lid == 'addressbook') // /addressbook/ contains all readably contacts
{
$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['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)
{
@ -1005,7 +1005,8 @@ class CalDAV extends HTTP_WebDAV_Server
$type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST', 'PROPPATCH']) ?
$_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;
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);
}
@ -1048,7 +1049,7 @@ class CalDAV extends HTTP_WebDAV_Server
{
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',
json_encode($data, self::JSON_OPTIONS_PRETTY));
@ -1066,9 +1067,10 @@ class CalDAV extends HTTP_WebDAV_Server
* }
*
* @param array $options
* @param bool $pretty =false true: pretty-print JSON
* @return bool|string|void
*/
protected function jsonIndex(array $options)
protected function jsonIndex(array $options, bool $pretty)
{
header('Content-Type: application/json; charset=utf-8');
$is_addressbook = strpos($options['path'], '/addressbook') !== false;
@ -1118,9 +1120,8 @@ class CalDAV extends HTTP_WebDAV_Server
{
return $ret; // no collection
}
echo "{\n";
$prefix = " ";
// set start as prefix, to no have it in front of exceptions
$prefix = "{\n\t\"responses\": {\n";
foreach($files['files'] as $resource)
{
$path = $resource['path'];
@ -1129,10 +1130,6 @@ class CalDAV extends HTTP_WebDAV_Server
{
echo 'null'; // deleted in sync-report
}
/*elseif (isset($resource['props']['address-data']))
{
echo $resource['props']['address-data']['val'];
}*/
else
{
$props = $propfind_options['props'] === 'all' ? $resource['props'] :
@ -1146,17 +1143,24 @@ class CalDAV extends HTTP_WebDAV_Server
{
$props = current($props)['val'];
}
echo self::json_encode($props);
echo self::json_encode($props, $pretty);
}
$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']))
{
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);
}
echo "\n}\n";
echo "\n}";
// exit now, so WebDAV::GET does NOT add Content-Type: application/octet-stream
exit;

View File

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

View File

@ -30,7 +30,7 @@ class JsContact
* Get jsCard for given 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
* @throws Api\Exception\NotFound
*/
@ -68,13 +68,13 @@ class JsContact
'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,
]),
'addresses' => [
'addresses' => array_filter([
'work' => self::address($contact, 'work', 1), // as it's the more prominent in our UI
'home' => self::address($contact, 'home'),
],
]),
'photos' => self::photos($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']),
'egroupware.org/customfields' => self::customfields($contact),
'egroupware.org/assistant' => $contact['assistent'],
@ -82,7 +82,7 @@ class JsContact
]);
if ($encode)
{
return Api\CalDAV::json_encode($data, self::JSON_OPTIONS_ERROR);
return Api\CalDAV::json_encode($data, $encode === "pretty");
}
return $data;
}
@ -474,16 +474,17 @@ class JsContact
$js2attr = self::$jsAddress2attr;
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];
}, $js2attr) + [
'street' => self::streetComponents($contact[$prefix.'street'], $contact[$prefix.'street2']),
]);
// only add contexts and preference to non-empty address
return !$address ? [] : $address+[
'contexts' => [$type => true],
'pref' => $preference,
];
return array_filter($address);
}
/**
@ -984,33 +985,35 @@ class JsContact
* Get jsCardGroup for given 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
*/
public static function getJsCardGroup($group)
public static function getJsCardGroup($group, $encode=true)
{
if (is_scalar($group) && !($group = self::getContacts()->read_lists($group)))
{
throw new Api\Exception\NotFound();
}
/*
$vCard = new Horde_Icalendar_Vcard($version);
$vCard->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['api']['version'].'//'.
strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
$vCard->setAttribute('N',$list['list_name'],array(),true,array($list['list_name'],'','','',''));
$vCard->setAttribute('FN',$list['list_name']);
$vCard->setAttribute('X-ADDRESSBOOKSERVER-KIND','group');
foreach($list['members'] as $uid)
$data = array_filter([
'uid' => $group['list_uid'],
'name' => $group['list_name'],
'card' => self::getJsCard([
'uid' => $group['list_uid'],
'n_fn' => $group['list_name'], // --> fullName
'modified' => $group['list_modified'], // no other way to send modification date
], false),
'members' => [],
]);
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'));
$vCard->setAttribute('UID',$list['list_uid']);
*/
return Api\CalDAV::json_encode($group, self::JSON_OPTIONS_ERROR);
if ($encode)
{
$data = Api\CalDAV::json_encode($data, $encode === 'pretty');
}
return $data;
}
/**

View File

@ -810,14 +810,14 @@ class Sql extends Api\Storage
{
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))
{
$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
{
$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",

View File

@ -1209,7 +1209,8 @@ class Storage
*
* @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 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
*/
function read_lists($keys,$member_attr=null,$limit_in_ab=false)