got POST, PUT and DELETE request to add, update and delete contacts working

added JSON exception handler with nicer JsCalendar parse errors
This commit is contained in:
Ralf Becker 2021-09-17 20:15:36 +02:00
parent 655f52a876
commit 3bc015a90d
4 changed files with 244 additions and 115 deletions

View File

@ -599,8 +599,10 @@ class addressbook_groupdav extends Api\CalDAV\Handler
// jsContact or vCard // jsContact or vCard
if (Api\CalDAV::isJSON()) if (Api\CalDAV::isJSON())
{ {
$options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact) : JsContact::getJsCard($contact); $options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact) :
$options['mimetype'] = $contact['list_id'] ? JsContact::MIME_TYPE_JSCARDGROUP : JsContact::MIME_TYPE_JSCARD; JsContact::getJsCard($contact);
$options['mimetype'] = ($contact['list_id'] ? JsContact::MIME_TYPE_JSCARDGROUP :
JsContact::MIME_TYPE_JSCARD).';charset=utf-8';
} }
else else
{ {
@ -638,10 +640,12 @@ class addressbook_groupdav extends Api\CalDAV\Handler
if (Api\CalDAV::isJSON()) if (Api\CalDAV::isJSON())
{ {
$contact = JsContact::parseJsCard($options['content']); $contact = JsContact::parseJsCard($options['content']);
// just output it again for now
/* uncomment to return parsed data for testing
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode($contact, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); echo json_encode($contact, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
return "200 Ok"; return "200 Ok";
*/
} }
else else
{ {
@ -751,7 +755,8 @@ 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);
$contact['photo_unchanged'] = false; // photo needs saving // ignore photo for JSON/REST, it's not yet supported
$contact['photo_unchanged'] = Api\CalDAV::isJSON(); //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");
@ -1051,7 +1056,17 @@ class addressbook_groupdav extends Api\CalDAV\Handler
unset($tids[Api\Contacts::DELETED_TYPE]); unset($tids[Api\Contacts::DELETED_TYPE]);
$non_deleted_tids = array_keys($tids); $non_deleted_tids = array_keys($tids);
} }
$contact = $this->bo->read(array(self::$path_attr => $id, '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
if ((string)$id === (string)(int)$id)
{
$keys['id'] = $id;
}
else
{
$keys[self::$path_attr] = $id;
}
$contact = $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'))))

View File

@ -19,16 +19,16 @@ use EGroupware\Api\CalDAV\Principals;
// explicit import non-namespaced classes // explicit import non-namespaced classes
require_once(__DIR__.'/WebDAV/Server.php'); require_once(__DIR__.'/WebDAV/Server.php');
use EGroupware\Api\Contacts\JsContact; use EGroupware\Api\Contacts\JsContactParseException;
use HTTP_WebDAV_Server; use HTTP_WebDAV_Server;
use calendar_hooks; use calendar_hooks;
/** /**
* EGroupware: GroupDAV access * EGroupware: CalDAV/CardDAV server
* *
* Using a modified PEAR HTTP/WebDAV/Server class from API! * Using a modified PEAR HTTP/WebDAV/Server class from API!
* *
* One can use the following url's releative (!) to http://domain.com/egroupware/groupdav.php * One can use the following URLs relative (!) to https://example.org/egroupware/groupdav.php
* *
* - / base of Cal|Card|GroupDAV tree, only certain clients (KDE, Apple) can autodetect folders from here * - / base of Cal|Card|GroupDAV tree, only certain clients (KDE, Apple) can autodetect folders from here
* - /principals/ principal-collection-set for WebDAV ACL * - /principals/ principal-collection-set for WebDAV ACL
@ -51,10 +51,25 @@ use calendar_hooks;
* - /(resources|locations)/<resource-name>/calendar calendar of a resource/location, if user has rights to view * - /(resources|locations)/<resource-name>/calendar calendar of a resource/location, if user has rights to view
* - /<current-username>/(resource|location)-<resource-name> shared calendar from a resource/location * - /<current-username>/(resource|location)-<resource-name> shared calendar from a resource/location
* *
* Shared addressbooks or calendars are only shown in in users home-set, if he subscribed to it via his CalDAV preferences! * Shared addressbooks or calendars are only shown in the users home-set, if he subscribed to it via his CalDAV preferences!
* *
* Calling one of the above collections with a GET request / regular browser generates an automatic index * Calling one of the above collections with a GET request / regular browser generates an automatic index
* from the data of a allprop PROPFIND, allow to browse CalDAV/CardDAV/GroupDAV tree with a regular browser. * from the data of a allprop PROPFIND, allow browsing CalDAV/CardDAV tree with a regular browser.
*
* Using EGroupware CalDAV/CardDAV as REST API: currently only for contacts
* ===========================================
* GET requests to collections with an "Accept: application/json" header return a JSON response similar to a PROPFIND
* following GET parameters are supported to customize the returned properties:
* - props[]=<DAV-prop-name> eg. props[]=getetag to return only the ETAG (multiple DAV properties can be specified)
* Default for addressbook collections is to only return address-data (JsContact), other collections return all props.
* - sync-token=<token> to only request change since last sync-token, like rfc6578 sync-collection REPORT
* - nresults=N limit number of responses (only for sync-collection / given sync-token parameter!)
* this will return a "more-results"=true attribute and a new "sync-token" attribute to query for the next chunk
* POST requests to collection with a "Content-Type: application/json" header add new entries in addressbook or calendar collections
* (Location header in response gives URL of new resource)
* GET requests with an "Accept: application/json" header can be used to retrieve single resources / JsContact or JsCalendar schema
* PUT requests with a "Content-Type: application/json" header allow modifying single resources
* DELETE requests delete single resources
* *
* Permanent error_log() calls should use groupdav->log($str) instead, to be send to PHP error_log() * Permanent error_log() calls should use groupdav->log($str) instead, to be send to PHP error_log()
* and our request-log (prefixed with "### " after request and response, like exceptions). * and our request-log (prefixed with "### " after request and response, like exceptions).
@ -1403,7 +1418,8 @@ class CalDAV extends HTTP_WebDAV_Server
{ {
// for some reason OS X Addressbook (CFNetwork user-agent) uses now (DAV:add-member given with collection URL+"?add-member") // for some reason OS X Addressbook (CFNetwork user-agent) uses now (DAV:add-member given with collection URL+"?add-member")
// POST to the collection URL plus a UID like name component (like for regular PUT) to create new entrys // POST to the collection URL plus a UID like name component (like for regular PUT) to create new entrys
if (isset($_GET['add-member']) || Handler::get_agent() == 'cfnetwork') if (isset($_GET['add-member']) || Handler::get_agent() == 'cfnetwork' ||
substr($options['path'], -1) === '/' && self::isJSON())
{ {
$_GET['add-member'] = ''; // otherwise we give no Location header $_GET['add-member'] = ''; // otherwise we give no Location header
return $this->PUT($options); return $this->PUT($options);
@ -2421,16 +2437,37 @@ class CalDAV extends HTTP_WebDAV_Server
$headline = null; $headline = null;
_egw_log_exception($e,$headline); _egw_log_exception($e,$headline);
// exception handler sending message back to the client as basic auth message if (self::isJSON())
$error = str_replace(array("\r", "\n"), array('', ' | '), $e->getMessage()); {
header('WWW-Authenticate: Basic realm="'.$headline.': '.$error.'"'); header('Content-Type: application/json; charset=utf-8');
header('HTTP/1.1 401 Unauthorized'); if (is_a($e, JsContactParseException::class))
header('X-WebDAV-Status: 401 Unauthorized', true); {
$status = '422 Unprocessable Entity';
}
else
{
$status = '500 Internal Server Error';
}
http_response_code((int)$status);
echo self::json_encode([
'error' => $e->getCode() ?: (int)$status,
'message' => $e->getMessage(),
]+($e->getPrevious() ? [
'original' => get_class($e->getPrevious()).': '.$e->getPrevious()->getMessage(),
] : []), JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
}
else
{
// exception handler sending message back to the client as basic auth message
$error = str_replace(array("\r", "\n"), array('', ' | '), $e->getMessage());
header('WWW-Authenticate: Basic realm="' . $headline . ': ' . $error . '"');
header('HTTP/1.1 401 Unauthorized');
header('X-WebDAV-Status: 401 Unauthorized', true);
}
// if our own logging is active, log the request plus a trace, if enabled in server-config // if our own logging is active, log the request plus a trace, if enabled in server-config
if (self::$request_starttime && isset(self::$instance)) if (self::$request_starttime && isset(self::$instance))
{ {
self::$instance->_http_status = '401 Unauthorized'; // to correctly log it self::$instance->_http_status = self::isJSON() ? $status : '401 Unauthorized'; // to correctly log it
if ($GLOBALS['egw_info']['server']['exception_show_trace']) if ($GLOBALS['egw_info']['server']['exception_show_trace'])
{ {
self::$instance->log_request("\n".$e->getTraceAsString()."\n"); self::$instance->log_request("\n".$e->getTraceAsString()."\n");

View File

@ -53,8 +53,15 @@ class JsContact
'organizations' => array_filter(['org' => self::organization($contact)]), 'organizations' => array_filter(['org' => self::organization($contact)]),
'titles' => self::titles($contact), 'titles' => self::titles($contact),
'emails' => array_filter([ 'emails' => array_filter([
'work' => empty($contact['email']) ? null : ['email' => $contact['email']], 'work' => empty($contact['email']) ? null : [
'private' => empty($contact['email_home']) ? null : ['email' => $contact['email_home']], 'email' => $contact['email'],
'contexts' => ['work' => true],
'pref' => 1, // as it's the more prominent in our UI
],
'private' => empty($contact['email_home']) ? null : [
'email' => $contact['email_home'],
'contexts' => ['private' => true],
],
]), ]),
'phones' => self::phones($contact), 'phones' => self::phones($contact),
'online' => array_filter([ 'online' => array_filter([
@ -62,7 +69,7 @@ class JsContact
'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' => [
'work' => self::address($contact, 'work', 1), '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),
@ -75,7 +82,7 @@ class JsContact
]); ]);
if ($encode) if ($encode)
{ {
return Api\CalDAV::json_encode($data); return Api\CalDAV::json_encode($data, self::JSON_OPTIONS_ERROR);
} }
return $data; return $data;
} }
@ -88,97 +95,126 @@ class JsContact
*/ */
public static function parseJsCard(string $json) public static function parseJsCard(string $json)
{ {
$data = json_decode($json, JSON_THROW_ON_ERROR); try
if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist
$contact = [];
foreach($data as $name => $value)
{ {
switch($name) $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
$contact = [];
foreach ($data as $name => $value)
{ {
case 'uid': switch ($name)
if (!is_string($value) || empty($value)) {
{ case 'uid':
throw new \InvalidArgumentException("Invalid uid value!"); if (!is_string($value) || empty($value))
} {
$contact['uid'] = $value; throw new \InvalidArgumentException("Missing or invalid uid value!");
break; }
$contact['uid'] = $value;
break;
case 'name': case 'name':
$contact += self::parseNameComponents($value); $contact += self::parseNameComponents($value);
break; break;
case 'fullName': case 'fullName':
$contact['n_fn'] = self::parseLocalizedString($value); $contact['n_fn'] = self::parseLocalizedString($value);
break; break;
case 'organizations': case 'organizations':
$contact += self::parseOrganizations($value); $contact += self::parseOrganizations($value);
break; break;
case 'titles': case 'titles':
$contact += self::parseTitles($value); $contact += self::parseTitles($value);
break; break;
case 'emails': case 'emails':
$contact += self::parseEmails($value); $contact += self::parseEmails($value);
break; break;
case 'phones': case 'phones':
$contact += self::parsePhones($value); $contact += self::parsePhones($value);
break; break;
case 'online': case 'online':
$contact += self::parseOnline($value); $contact += self::parseOnline($value);
break; break;
case 'addresses': case 'addresses':
$contact += self::parseAddresses($value); $contact += self::parseAddresses($value);
break; break;
case 'photos': case 'photos':
$contact += self::parsePhotos($value); $contact += self::parsePhotos($value);
break; break;
case 'anniversaries': case 'anniversaries':
$contact += self::parseAnniversaries($value); $contact += self::parseAnniversaries($value);
break; break;
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::parseLocalizedString($note); }, $value));
}, $value)); break;
break;
case 'categories': case 'categories':
$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;
case 'prodId': case 'created': case 'updated': case 'kind': case 'prodId':
break; case 'created':
case 'updated':
case 'kind':
break;
default: default:
error_log(__METHOD__."() $name=".json_encode($value).' --> ignored'); error_log(__METHOD__ . "() $name=" . json_encode($value, self::JSON_OPTIONS_ERROR) . ' --> ignored');
break; break;
}
} }
} }
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);
}
return $contact; return $contact;
} }
/**
* JSON options for errors thrown as exceptions
*/
const JSON_OPTIONS_ERROR = JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE;
/** /**
* Return organisation * Return organisation
* *
@ -317,6 +353,7 @@ class JsContact
* Parse custom fields * Parse custom fields
* *
* Not defined custom fields are ignored! * Not defined custom fields are ignored!
* Not send custom fields are set to null!
* *
* @param array $cfs name => object with attribute data and optional type, label, values * @param array $cfs name => object with attribute data and optional type, label, values
* @return array * @return array
@ -324,17 +361,18 @@ class JsContact
protected static function parseCustomfields(array $cfs) protected static function parseCustomfields(array $cfs)
{ {
$contact = []; $contact = [];
$definition = Api\Storage\Customfields::get('addressbook'); $definitions = Api\Storage\Customfields::get('addressbook');
foreach($cfs as $name => $data) foreach($definitions as $name => $definition)
{ {
if (!is_array($data) || !array_key_exists('value', $data)) $data = $cfs[$name];
if (isset($data[$name]))
{ {
throw new \InvalidArgumentException("Invalid customfield object $name: ".json_encode($data)); if (!is_array($data) || !array_key_exists('value', $data))
} {
if (isset($definition[$name])) throw new \InvalidArgumentException("Invalid customfield object $name: ".json_encode($data, self::JSON_OPTIONS_ERROR));
{ }
switch($definition[$name]['type']) switch($definition['type'])
{ {
case 'date-time': case 'date-time':
$data['value'] = Api\DateTime::to($data['value'], 'object'); $data['value'] = Api\DateTime::to($data['value'], 'object');
@ -347,12 +385,22 @@ class JsContact
break; break;
case 'select': case 'select':
if (is_scalar($data['value'])) $data['value'] = explode(',', $data['value']); if (is_scalar($data['value'])) $data['value'] = explode(',', $data['value']);
$data['value'] = array_intersect(array_keys($definition[$name]['values']), $data['value']); $data['value'] = array_intersect(array_keys($definition['values']), $data['value']);
$data['value'] = $data['value'] ? implode(',', (array)$data['value']) : null; $data['value'] = $data['value'] ? implode(',', (array)$data['value']) : null;
break; break;
} }
$contact['#'.$name] = $data['value']; $contact['#'.$name] = $data['value'];
} }
// set not return cfs to null
else
{
$contact['#'.$name] = null;
}
}
// report not existing cfs to log
if (($not_existing=array_diff(array_keys($cfs), array_keys($definitions))))
{
error_log(__METHOD__."() not existing/ignored custom fields: ".implode(', ', $not_existing));
} }
return $contact; return $contact;
} }
@ -454,7 +502,7 @@ class JsContact
$contact += ($values=self::parseAddress($address, $id, $last_type)); $contact += ($values=self::parseAddress($address, $id, $last_type));
if (++$n > 2) if (++$n > 2)
{ {
error_log(__METHOD__."() Ignoring $n. address id=$id: ".json_encode($address)); error_log(__METHOD__."() Ignoring $n. address id=$id: ".json_encode($address, self::JSON_OPTIONS_ERROR));
break; break;
} }
} }
@ -481,7 +529,8 @@ class JsContact
*/ */
protected static function parseAddress(array $address, string $id, string &$last_type=null) protected static function parseAddress(array $address, string $id, string &$last_type=null)
{ {
$type = !isset($last_type) && (empty($address['context']['private']) || $id !== 'private') || $last_type === 'home' ? 'work' : 'home'; $type = !isset($last_type) && (empty($address['contexts']['private']) || $id === 'work') ||
$last_type === 'home' ? 'work' : 'home';
$last_type = $type; $last_type = $type;
$prefix = $type === 'work' ? 'adr_one_' : 'adr_two_'; $prefix = $type === 'work' ? 'adr_one_' : 'adr_two_';
@ -547,7 +596,7 @@ class JsContact
{ {
if (!is_array($component) || !is_string($component['value'])) if (!is_array($component) || !is_string($component['value']))
{ {
throw new \InvalidArgumentException("Invalid streetComponents!"); throw new \InvalidArgumentException("Invalid street-component: ".json_encode($component, self::JSON_OPTIONS_ERROR));
} }
if ($street && $last_type !== 'separator') // if we have no separator, we add a space if ($street && $last_type !== 'separator') // if we have no separator, we add a space
{ {
@ -614,7 +663,7 @@ class JsContact
{ {
if (!is_array($phone) || !is_string($phone['phone'])) if (!is_array($phone) || !is_string($phone['phone']))
{ {
throw new \InvalidArgumentException("Invalid phone: " . json_encode($phone)); throw new \InvalidArgumentException("Invalid phone: " . json_encode($phone, self::JSON_OPTIONS_ERROR));
} }
// first check for "our" id's // first check for "our" id's
if (isset(self::$phone2jscard[$id]) && !isset($contact[$id])) if (isset(self::$phone2jscard[$id]) && !isset($contact[$id]))
@ -687,7 +736,7 @@ class JsContact
{ {
if (!is_array($value) || !is_string($value['resource'])) if (!is_array($value) || !is_string($value['resource']))
{ {
throw new \InvalidArgumentException("Invalid online resource with id '$id': ".json_encode($value)); throw new \InvalidArgumentException("Invalid online resource with id '$id': ".json_encode($value, self::JSON_OPTIONS_ERROR));
} }
// check for "our" id's // check for "our" id's
if (in_array($id, ['url', 'url_home'])) if (in_array($id, ['url', 'url_home']))
@ -717,6 +766,7 @@ class JsContact
/** /**
* Parse emails object * Parse emails object
* *
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-2.3.1
* @param array $emails id => object with attribute "email" and optional "context" * @param array $emails id => object with attribute "email" and optional "context"
* @return array * @return array
*/ */
@ -727,7 +777,7 @@ class JsContact
{ {
if (!is_array($value) || !is_string($value['email'])) if (!is_array($value) || !is_string($value['email']))
{ {
throw new \InvalidArgumentException("Invalid email object: ".json_encode($value)); throw new \InvalidArgumentException("Invalid email object (requires email attribute): ".json_encode($value, self::JSON_OPTIONS_ERROR));
} }
if (!isset($contact['email']) && $id !== 'private' && empty($value['context']['private'])) if (!isset($contact['email']) && $id !== 'private' && empty($value['context']['private']))
{ {
@ -802,21 +852,21 @@ class JsContact
/** /**
* parse nameComponents * parse nameComponents
* *
* @param array $values * @param array $components
* @return array * @return array
*/ */
protected static function parseNameComponents(array $values) protected static function parseNameComponents(array $components)
{ {
$contact = array_combine(array_values(self::$nameType2attribute), $contact = array_combine(array_values(self::$nameType2attribute),
array_fill(0, count(self::$nameType2attribute), null)); array_fill(0, count(self::$nameType2attribute), null));
foreach($values as $value) foreach($components as $component)
{ {
if (empty($value['type']) || isset($value) && !is_string($value['value'])) if (empty($component['type']) || isset($component) && !is_string($component['value']))
{ {
throw new \InvalidArgumentException("Invalid nameComponent!"); throw new \InvalidArgumentException("Invalid name-component (must have type and value attributes): ".json_encode($component, self::JSON_OPTIONS_ERROR));
} }
$contact[self::$nameType2attribute[$value['type']]] = $value['value']; $contact[self::$nameType2attribute[$component['type']]] = $component['value'];
} }
return $contact; return $contact;
} }
@ -854,7 +904,7 @@ class JsContact
(!list($year, $month, $day) = explode('-', $anniversary['date'])) || (!list($year, $month, $day) = explode('-', $anniversary['date'])) ||
!(1 <= $month && $month <= 12 && 1 <= $day && $day <= 31)) !(1 <= $month && $month <= 12 && 1 <= $day && $day <= 31))
{ {
throw new \InvalidArgumentException("Invalid anniversary object with id '$id': ".json_encode($anniversary)); throw new \InvalidArgumentException("Invalid anniversary object with id '$id': ".json_encode($anniversary, self::JSON_OPTIONS_ERROR));
} }
if (!isset($contact['bday']) && ($id === 'bday' || $anniversary['type'] === 'birth')) if (!isset($contact['bday']) && ($id === 'bday' || $anniversary['type'] === 'birth'))
{ {
@ -862,7 +912,7 @@ class JsContact
} }
else else
{ {
error_log(__METHOD__."() only one birtday is supported, ignoring aniversary: ".json_encode($anniversary)); error_log(__METHOD__."() only one birtday is supported, ignoring aniversary: ".json_encode($anniversary, self::JSON_OPTIONS_ERROR));
} }
} }
return $contact; return $contact;
@ -902,7 +952,7 @@ class JsContact
{ {
if (!is_string($value['value'])) if (!is_string($value['value']))
{ {
throw new \InvalidArgumentException("Invalid localizedString: ".json_encode($value)); throw new \InvalidArgumentException("Invalid localizedString: ".json_encode($value, self::JSON_OPTIONS_ERROR));
} }
return $value['value']; return $value['value'];
} }
@ -960,7 +1010,7 @@ class JsContact
$vCard->setAttribute('UID',$list['list_uid']); $vCard->setAttribute('UID',$list['list_uid']);
*/ */
return Api\CalDAV::json_encode($group); return Api\CalDAV::json_encode($group, self::JSON_OPTIONS_ERROR);
} }
/** /**
@ -975,4 +1025,4 @@ class JsContact
} }
return $contacts; return $contacts;
} }
} }

View File

@ -0,0 +1,27 @@
<?php
/**
* EGroupware API - JsContact
*
* @link https://www.egroupware.org
* @author Ralf Becker <rb@egroupware.org>
* @package addressbook
* @copyright (c) 2021 by Ralf Becker <rb@egroupware.org>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\Api\Contacts;
use Throwable;
/**
* Error parsing JsContact format
*
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07
*/
class JsContactParseException extends \InvalidArgumentException
{
public function __construct($message = "", $code = 422, Throwable $previous = null)
{
parent::__construct($message, $code ?: 422, $previous);
}
}