mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-21 23:43:17 +01:00
WIP REST Api for contacts
This commit is contained in:
parent
fc3aba9c39
commit
0768f5fadf
@ -73,9 +73,14 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
|
||||
$this->bo = new Api\Contacts();
|
||||
|
||||
if (Api\CalDAV::isJSON())
|
||||
{
|
||||
self::$path_attr = 'id';
|
||||
self::$path_extension = '';
|
||||
}
|
||||
// since 1.9.007 we allow clients to specify the URL when creating a new contact, as specified by CardDAV
|
||||
// LDAP does NOT have a carddav_name attribute --> stick with id mapped to LDAP attribute uid
|
||||
if (version_compare($GLOBALS['egw_info']['apps']['api']['version'], '1.9.007', '<') ||
|
||||
elseif (version_compare($GLOBALS['egw_info']['apps']['api']['version'], '1.9.007', '<') ||
|
||||
$this->bo->contact_repository != 'sql' ||
|
||||
$this->bo->account_repository != 'sql' && strpos($_SERVER['REQUEST_URI'].'/','/addressbook-accounts/') !== false)
|
||||
{
|
||||
@ -173,12 +178,12 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
if ($options['root']['name'] == 'sync-collection' && $this->bo->total > $nresults)
|
||||
{
|
||||
--$this->sync_collection_token;
|
||||
$files['sync-token-params'][] = true; // tel get_sync_collection_token that we have more entries
|
||||
$files['sync-token-params'][] = true; // tell get_sync_collection_token that we have more entries
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// return iterator, calling ourself to return result in chunks
|
||||
// return iterator, calling ourselves to return result in chunks
|
||||
$files['files'] = new Api\CalDAV\PropfindIterator($this,$path,$filter,$files['files']);
|
||||
}
|
||||
return true;
|
||||
@ -270,6 +275,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
}
|
||||
}
|
||||
|
||||
$is_jscontact = Api\CalDAV::isJSON();
|
||||
foreach($contacts as &$contact)
|
||||
{
|
||||
// remove contact from requested multiget ids, to be able to report not found urls
|
||||
@ -284,15 +290,16 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
continue;
|
||||
}
|
||||
$props = array(
|
||||
'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', 'text/vcard'),
|
||||
'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', $is_jscontact ? JsContact::MIME_TYPE_JSCARD : 'text/vcard'),
|
||||
'getlastmodified' => $contact['modified'],
|
||||
'displayname' => $contact['n_fn'],
|
||||
);
|
||||
if ($address_data)
|
||||
{
|
||||
$content = $handler->getVCard($contact['id'],$this->charset,false);
|
||||
$content = $is_jscontact ? JsContact::getJsCard($contact['id'], false) :
|
||||
$handler->getVCard($contact['id'],$this->charset,false);
|
||||
$props['getcontentlength'] = bytes($content);
|
||||
$props[] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
|
||||
$props['address-data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
|
||||
}
|
||||
$files[] = $this->add_resource($path, $contact, $props);
|
||||
}
|
||||
@ -351,16 +358,16 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
$etag .= ':'.implode('-',$filter['owner']);
|
||||
}
|
||||
$props = array(
|
||||
'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', 'text/vcard'),
|
||||
'getcontenttype' => Api\CalDAV::mkprop('getcontenttype', $is_jscontact ? JsContact::MIME_TYPE_JSCARDGROUP : 'text/vcard'),
|
||||
'getlastmodified' => Api\DateTime::to($list['list_modified'],'ts'),
|
||||
'displayname' => $list['list_name'],
|
||||
'getetag' => '"'.$etag.'"',
|
||||
);
|
||||
if ($address_data)
|
||||
{
|
||||
$content = $handler->getGroupVCard($list);
|
||||
$content = $is_jscontact ? JsContact::getJsCardGroup($list) : $handler->getGroupVCard($list);
|
||||
$props['getcontentlength'] = bytes($content);
|
||||
$props[] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
|
||||
$props['address-data'] = Api\CalDAV::mkprop(Api\CalDAV::CARDDAV, 'address-data', $content);
|
||||
}
|
||||
$files[] = $this->add_resource($path, $list, $props);
|
||||
|
||||
@ -452,7 +459,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
}
|
||||
else
|
||||
{
|
||||
switch($filter['attrs']['collation']) // todo: which other collations allowed, we are allways unicode
|
||||
switch($filter['attrs']['collation']) // todo: which other collations allowed, we are always unicode
|
||||
{
|
||||
case 'i;unicode-casemap':
|
||||
default:
|
||||
@ -590,7 +597,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
return $contact;
|
||||
}
|
||||
// jsContact or vCard
|
||||
if (JsContact::isJsContact())
|
||||
if (Api\CalDAV::isJSON())
|
||||
{
|
||||
$options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact) : JsContact::getJsCard($contact);
|
||||
$options['mimetype'] = $contact['list_id'] ? JsContact::MIME_TYPE_JSCARDGROUP : JsContact::MIME_TYPE_JSCARD;
|
||||
@ -628,7 +635,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
||||
return $oldContact;
|
||||
}
|
||||
|
||||
if (JsContact::isJsContact())
|
||||
if (Api\CalDAV::isJSON())
|
||||
{
|
||||
$contact = JsContact::parseJsCard($options['content']);
|
||||
// just output it again for now
|
||||
|
@ -18,6 +18,8 @@ use EGroupware\Api\CalDAV\Principals;
|
||||
|
||||
// explicit import non-namespaced classes
|
||||
require_once(__DIR__.'/WebDAV/Server.php');
|
||||
|
||||
use EGroupware\Api\Contacts\JsContact;
|
||||
use HTTP_WebDAV_Server;
|
||||
use calendar_hooks;
|
||||
|
||||
@ -976,6 +978,21 @@ class CalDAV extends HTTP_WebDAV_Server
|
||||
parent::http_PROPFIND('REPORT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if clients want's or sends JSON
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isJSON(string $type=null)
|
||||
{
|
||||
if (!isset($type))
|
||||
{
|
||||
$type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST', 'PROPPATCH']) ?
|
||||
$_SERVER['HTTP_CONTENT_TYPE'] : $_SERVER['HTTP_ACCEPT'];
|
||||
}
|
||||
return (bool)preg_match('#application/([^+ ;]+\+)?json#', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET method handler
|
||||
*
|
||||
@ -989,6 +1006,10 @@ class CalDAV extends HTTP_WebDAV_Server
|
||||
$id = $app = $user = null;
|
||||
if (!$this->_parse_path($options['path'],$id,$app,$user) || $app == 'principals')
|
||||
{
|
||||
if (self::isJSON())
|
||||
{
|
||||
return $this->jsonIndex($options);
|
||||
}
|
||||
return $this->autoindex($options);
|
||||
}
|
||||
if (($handler = self::app_handler($app)))
|
||||
@ -999,6 +1020,170 @@ class CalDAV extends HTTP_WebDAV_Server
|
||||
return '501 Not Implemented';
|
||||
}
|
||||
|
||||
const JSON_OPTIONS = JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR;
|
||||
const JSON_OPTIONS_PRETTY = JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR;
|
||||
|
||||
/**
|
||||
* JSON encode incl. modified pretty-print
|
||||
*
|
||||
* @param $data
|
||||
* @return array|string|string[]|null
|
||||
*/
|
||||
public static function json_encode($data, $pretty = true)
|
||||
{
|
||||
if (!$pretty)
|
||||
{
|
||||
return self::json_encode($data, self::JSON_OPTIONS);
|
||||
}
|
||||
return preg_replace('/: {\n\s*(.*?)\n\s*(},?\n)/', ': { $1 $2',
|
||||
json_encode($data, self::JSON_OPTIONS_PRETTY));
|
||||
}
|
||||
|
||||
/**
|
||||
* PROPFIND/REPORT like output for GET request on collection with Accept: application/(.*+)?json
|
||||
*
|
||||
* For addressbook-collections we give a REST-like output without any other properties
|
||||
* {
|
||||
* "/addressbook/ID": {
|
||||
* JsContact-data
|
||||
* },
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* @param array $options
|
||||
* @return bool|string|void
|
||||
*/
|
||||
protected function jsonIndex(array $options)
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$is_addressbook = strpos($options['path'], '/addressbook') !== false;
|
||||
$propfind_options = array(
|
||||
'path' => $options['path'],
|
||||
'depth' => 1,
|
||||
'props' => $is_addressbook ? [
|
||||
'address-data' => self::mkprop(self::CARDDAV, 'address-data', '')
|
||||
] : 'all',
|
||||
'other' => [],
|
||||
);
|
||||
|
||||
// sync-collection report via GET parameter sync-token
|
||||
if (isset($_GET['sync-token']))
|
||||
{
|
||||
$propfind_options['root'] = ['name' => 'sync-collection'];
|
||||
$propfind_options['other'][] = ['name' => 'sync-token', 'data' => $_GET['sync-token']];
|
||||
$propfind_options['other'][] = ['name' => 'sync-level', 'data' => $_GET['sync-level'] ?? 1];
|
||||
|
||||
// clients want's pagination
|
||||
if (isset($_GET['nresults']))
|
||||
{
|
||||
$propfind_options['other'][] = ['name' => 'nresults', 'data' => (int)$_GET['nresults']];
|
||||
}
|
||||
}
|
||||
|
||||
// ToDo: client want data filtered
|
||||
if (isset($_GET['filters']))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// properties to NOT get the default address-data for addressbook-collections and "all" for the rest
|
||||
if (isset($_GET['props']))
|
||||
{
|
||||
$propfind_options['props'] = [];
|
||||
foreach((array)$_GET['props'] as $value)
|
||||
{
|
||||
$parts = explode(':', $value);
|
||||
$name = array_pop($parts);
|
||||
$ns = $parts ? implode(':', $parts) : 'DAV:';
|
||||
$propfind_options['props'][$name] = self::mkprop($ns, $name, '');
|
||||
}
|
||||
}
|
||||
$files = array();
|
||||
if (($ret = $this->REPORT($propfind_options,$files)) !== true)
|
||||
{
|
||||
return $ret; // no collection
|
||||
}
|
||||
|
||||
echo "{\n";
|
||||
$prefix = " ";
|
||||
foreach($files['files'] as $resource)
|
||||
{
|
||||
$path = $resource['path'];
|
||||
echo $prefix.json_encode($path, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).': ';
|
||||
if (!isset($resource['props']))
|
||||
{
|
||||
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'] :
|
||||
array_intersect_key($resource['props'], $propfind_options['props']);
|
||||
|
||||
if (count($props) > 1)
|
||||
{
|
||||
$props = self::jsonProps($props);
|
||||
}
|
||||
else
|
||||
{
|
||||
$props = current($props)['val'];
|
||||
}
|
||||
echo self::json_encode($props);
|
||||
}
|
||||
$prefix = ",\n ";
|
||||
}
|
||||
// add sync-token to response
|
||||
if (isset($files['sync-token']))
|
||||
{
|
||||
echo $prefix.'"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";
|
||||
|
||||
// exit now, so WebDAV::GET does NOT add Content-Type: application/octet-stream
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nicer way to display/encode DAV properties
|
||||
*
|
||||
* @param array $props
|
||||
* @return array
|
||||
*/
|
||||
protected function jsonProps(array $props)
|
||||
{
|
||||
$json = [];
|
||||
foreach($props as $key => $prop)
|
||||
{
|
||||
if (is_scalar($prop['val']))
|
||||
{
|
||||
$value = is_int($key) && $prop['val'] === '' ?
|
||||
/*$prop['ns'].':'.*/$prop['name'] : $prop['val'];
|
||||
}
|
||||
// check if this is a property-object
|
||||
elseif (count($prop) === 3 && isset($prop['name']) && isset($prop['ns']) && isset($prop['val']))
|
||||
{
|
||||
$value = $prop['name'] === 'address-data' ? $prop['val'] : self::jsonProps($prop['val']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$value = $prop;
|
||||
}
|
||||
if (is_int($key))
|
||||
{
|
||||
$json[] = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$json[/*($prop['ns'] === 'DAV:' ? '' : $prop['ns'].':').*/$prop['name']] = $value;
|
||||
}
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an automatic index (listing and properties) for a collection
|
||||
*
|
||||
|
@ -712,16 +712,23 @@ abstract class Handler
|
||||
//error_log(__METHOD__."('$path', $user, more_results=$more_results) this->sync_collection_token=".$this->sync_collection_token);
|
||||
if ($more_results)
|
||||
{
|
||||
$error =
|
||||
' <D:response>
|
||||
<D:href>'.htmlspecialchars($this->caldav->base_uri.$this->caldav->path).'</D:href>
|
||||
if (Api\CalDAV::isJSON())
|
||||
{
|
||||
$error = ",\n".' "more-results": true';
|
||||
}
|
||||
else
|
||||
{
|
||||
$error =
|
||||
' <D:response>
|
||||
<D:href>' . htmlspecialchars($this->caldav->base_uri . $this->caldav->path) . '</D:href>
|
||||
<D:status>HTTP/1.1 507 Insufficient Storage</D:status>
|
||||
<D:error><D:number-of-matches-within-limits/></D:error>
|
||||
</D:response>
|
||||
';
|
||||
if ($this->caldav->crrnd)
|
||||
{
|
||||
$error = str_replace(array('<D:', '</D:'), array('<', '</'), $error);
|
||||
if ($this->caldav->crrnd)
|
||||
{
|
||||
$error = str_replace(array('<D:', '</D:'), array('<', '</'), $error);
|
||||
}
|
||||
}
|
||||
echo $error;
|
||||
}
|
||||
|
@ -26,39 +26,21 @@ class JsContact
|
||||
const MIME_TYPE_JSCARDGROUP = "application/jscontact+json;type=cardgroup";
|
||||
const MIME_TYPE_JSON = "application/json";
|
||||
|
||||
/**
|
||||
* Check if request want's JSON
|
||||
*
|
||||
* @param ?string $type default use Content-Type or Accept HTTP header depending on request method
|
||||
* @return bool true: jsContact, false: other eg. vCard
|
||||
*/
|
||||
public static function isJsContact(string $type=null)
|
||||
{
|
||||
if (!isset($type))
|
||||
{
|
||||
$type = in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'POST']) ?
|
||||
$_SERVER['HTTP_CONTENT_TYPE'] : $_SERVER['HTTP_ACCEPT'];
|
||||
}
|
||||
return strpos($type, self::MIME_TYPE) !== false ||
|
||||
strpos($type, self::MIME_TYPE_JSON) !== false;
|
||||
}
|
||||
|
||||
const JSON_OPTIONS = JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE;
|
||||
|
||||
/**
|
||||
* Get jsCard for given contact
|
||||
*
|
||||
* @param int|array $contact
|
||||
* @return string
|
||||
* @param bool $encode=true true: JSON encode, false: return raw data eg. from listing
|
||||
* @return string|array
|
||||
* @throws Api\Exception\NotFound
|
||||
*/
|
||||
public static function getJsCard($contact)
|
||||
public static function getJsCard($contact, $encode=true)
|
||||
{
|
||||
if (is_scalar($contact) && !($contact = self::getContacts()->read($contact)))
|
||||
{
|
||||
throw new Api\Exception\NotFound();
|
||||
}
|
||||
return json_encode(array_filter([
|
||||
$data = array_filter([
|
||||
'uid' => $contact['uid'],
|
||||
'prodId' => 'EGroupware Addressbook '.$GLOBALS['egw_info']['apps']['api']['version'],
|
||||
'created' => self::UTCDateTime($contact['created']),
|
||||
@ -90,7 +72,12 @@ class JsContact
|
||||
'egroupware.org/customfields' => self::customfields($contact),
|
||||
'egroupware.org/assistant' => $contact['assistent'],
|
||||
'egroupware.org/fileAs' => $contact['fileas'],
|
||||
]), self::JSON_OPTIONS);
|
||||
]);
|
||||
if ($encode)
|
||||
{
|
||||
return Api\CalDAV::json_encode($data);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -526,14 +513,19 @@ class JsContact
|
||||
* @param ?string $street2=null 2. address line
|
||||
* @return array[] array of objects with attributes type and value
|
||||
*/
|
||||
protected static function streetComponents(string $street, ?string $street2=null)
|
||||
protected static function streetComponents(?string $street, ?string $street2=null)
|
||||
{
|
||||
$components = [['type' => 'name', 'value' => $street]];
|
||||
|
||||
if (!empty($street2))
|
||||
$components = [];
|
||||
foreach(func_get_args() as $street)
|
||||
{
|
||||
$components[] = ['type' => 'separator', 'value' => "\n"];
|
||||
$components[] = ['type' => 'name', 'value' => $street2];
|
||||
if ($components)
|
||||
{
|
||||
$components[] = ['type' => 'separator', 'value' => "\n"];
|
||||
}
|
||||
if (!empty($street))
|
||||
{
|
||||
$components[] = ['type' => 'name', 'value' => $street];
|
||||
}
|
||||
}
|
||||
return $components;
|
||||
}
|
||||
@ -968,7 +960,7 @@ class JsContact
|
||||
$vCard->setAttribute('UID',$list['list_uid']);
|
||||
*/
|
||||
|
||||
return json_encode($group, self::JSON_OPTIONS);
|
||||
return Api\CalDAV::json_encode($group);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user