mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-28 10:53:39 +01:00
WIP REST Api for Timesheet app
This commit is contained in:
parent
0ad1729fe0
commit
2aedd7f5ef
@ -19,7 +19,7 @@ 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\JsContactParseException;
|
use EGroupware\Api\CalDAV\JsParseException;
|
||||||
use HTTP_WebDAV_Server;
|
use HTTP_WebDAV_Server;
|
||||||
use calendar_hooks;
|
use calendar_hooks;
|
||||||
|
|
||||||
@ -1156,7 +1156,9 @@ class CalDAV extends HTTP_WebDAV_Server
|
|||||||
'address-data' => self::mkprop(self::CARDDAV, 'address-data', '')
|
'address-data' => self::mkprop(self::CARDDAV, 'address-data', '')
|
||||||
] : ($is_calendar ? [
|
] : ($is_calendar ? [
|
||||||
'calendar-data' => self::mkprop(self::CALDAV, 'calendar-data', ''),
|
'calendar-data' => self::mkprop(self::CALDAV, 'calendar-data', ''),
|
||||||
] : 'all'),
|
] : [
|
||||||
|
'data' => self::mkprop(self::CALDAV, 'data', '')
|
||||||
|
]),
|
||||||
'other' => [],
|
'other' => [],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1262,7 +1264,7 @@ class CalDAV extends HTTP_WebDAV_Server
|
|||||||
// check if this is a property-object
|
// check if this is a property-object
|
||||||
elseif (count($prop) === 3 && isset($prop['name']) && isset($prop['ns']) && isset($prop['val']))
|
elseif (count($prop) === 3 && isset($prop['name']) && isset($prop['ns']) && isset($prop['val']))
|
||||||
{
|
{
|
||||||
$value = in_array($prop['name'], ['address-data', 'calendar-data']) ? $prop['val'] : self::jsonProps($prop['val']);
|
$value = in_array($prop['name'], ['address-data', 'calendar-data', 'data']) ? $prop['val'] : self::jsonProps($prop['val']);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -2557,7 +2559,7 @@ class CalDAV extends HTTP_WebDAV_Server
|
|||||||
if (self::isJSON())
|
if (self::isJSON())
|
||||||
{
|
{
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
if (is_a($e, JsContactParseException::class))
|
if (is_a($e, JsParseException::class))
|
||||||
{
|
{
|
||||||
$status = '422 Unprocessable Entity';
|
$status = '422 Unprocessable Entity';
|
||||||
}
|
}
|
||||||
|
@ -561,7 +561,7 @@ abstract class Handler
|
|||||||
*/
|
*/
|
||||||
function get_path($entry)
|
function get_path($entry)
|
||||||
{
|
{
|
||||||
return (is_array($entry) ? $entry[self::$path_attr] : $entry).self::$path_extension;
|
return (is_array($entry) ? $entry[static::$path_attr] : $entry).static::$path_extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
387
api/src/CalDAV/JsBase.php
Normal file
387
api/src/CalDAV/JsBase.php
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* EGroupware REST API - JsBase base-class
|
||||||
|
*
|
||||||
|
* @link https://www.egroupware.org
|
||||||
|
* @author Ralf Becker <rb@egroupware.org>
|
||||||
|
* @package calendar
|
||||||
|
* @copyright (c) 2023 by Ralf Becker <rb@egroupware.org>
|
||||||
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace EGroupware\Api\CalDAV;
|
||||||
|
|
||||||
|
use EGroupware\Api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared base-class of all REST API / JMAP classes
|
||||||
|
*
|
||||||
|
* @link https://datatracker.ietf.org/doc/html/rfc8984
|
||||||
|
* @link https://jmap.io/spec-calendars.html
|
||||||
|
*/
|
||||||
|
class JsBase
|
||||||
|
{
|
||||||
|
const APP = null;
|
||||||
|
|
||||||
|
const MIME_TYPE_JSON = "application/json";
|
||||||
|
|
||||||
|
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 string|null $old old value, if given it must NOT change
|
||||||
|
* @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, string $old=null, bool $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();
|
||||||
|
}
|
||||||
|
if (strpos($uid, self::URN_UUID_PREFIX) === 0)
|
||||||
|
{
|
||||||
|
$uid = substr($uid, strlen(self::URN_UUID_PREFIX));
|
||||||
|
}
|
||||||
|
if (isset($old) && $old !== $uid)
|
||||||
|
{
|
||||||
|
throw new \InvalidArgumentException("You must NOT change the UID ('$old'): ".json_encode($uid));
|
||||||
|
}
|
||||||
|
return $uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a date-time value in UTC
|
||||||
|
*
|
||||||
|
* @link https://datatracker.ietf.org/doc/html/rfc8984#section-1.4.4
|
||||||
|
* @param null|string|\DateTime $date
|
||||||
|
* @param bool $user false: timestamp in server-time, true: timestamp in user-time, does NOT matter for DateTime objects
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
protected static function UTCDateTime($date, bool $user=false)
|
||||||
|
{
|
||||||
|
static $utc=null;
|
||||||
|
if (!isset($utc)) $utc = new \DateTimeZone('UTC');
|
||||||
|
|
||||||
|
if (!isset($date))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$date = $user ? Api\DateTime::user2server($date, 'object') : Api\DateTime::to($date, 'object');
|
||||||
|
$date->setTimezone($utc);
|
||||||
|
|
||||||
|
// we need to use "Z", not "+00:00"
|
||||||
|
return substr($date->format(Api\DateTime::RFC3339), 0, -6).'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output an account as email, if available, username or as last resort the numerical account_id
|
||||||
|
*
|
||||||
|
* @param int|null $account_id
|
||||||
|
* @return string|int|null
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected static function account(int $account_id=null)
|
||||||
|
{
|
||||||
|
if (!$account_id)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Api\Accounts::id2name($account_id, 'account_email') ?: Api\Accounts::id2name($account_id) ?: $account_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an account specified as email, account_lid or account_id
|
||||||
|
*
|
||||||
|
* @param string|int $value
|
||||||
|
* @param ?bool $user
|
||||||
|
* @return int
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected static function parseAccount(string $value, bool $user=true)
|
||||||
|
{
|
||||||
|
if (is_numeric($value) && ($exists = Api\Accounts::getInstance()->exists(value)) &&
|
||||||
|
(!isset($user) || $exists === ($user ? 1 : 2)))
|
||||||
|
{
|
||||||
|
$account_id = (int)$value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$account_id = Api\Accounts::getInstance()->name2id($value,
|
||||||
|
strpos($value, '@') !== false ? 'account_email' : 'account_lid',
|
||||||
|
isset($user) ? ($user ? 'u' : 'g') : null);
|
||||||
|
}
|
||||||
|
if (!$account_id)
|
||||||
|
{
|
||||||
|
throw new JsParseException("Invalid or non-existing account '$value'");
|
||||||
|
}
|
||||||
|
return $account_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON options for errors thrown as exceptions
|
||||||
|
*/
|
||||||
|
const JSON_OPTIONS_ERROR = JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE;
|
||||||
|
|
||||||
|
const AT_TYPE = '@type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return EGroupware custom fields
|
||||||
|
*
|
||||||
|
* @param array $contact
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected static function customfields(array $contact)
|
||||||
|
{
|
||||||
|
$fields = [];
|
||||||
|
foreach(Api\Storage\Customfields::get(static::APP) as $name => $data)
|
||||||
|
{
|
||||||
|
$value = $contact['#'.$name];
|
||||||
|
if (isset($value))
|
||||||
|
{
|
||||||
|
switch($data['type'])
|
||||||
|
{
|
||||||
|
case 'date-time':
|
||||||
|
$value = Api\DateTime::to($value, Api\DateTime::RFC3339);
|
||||||
|
break;
|
||||||
|
case 'float':
|
||||||
|
$value = (double)$value;
|
||||||
|
break;
|
||||||
|
case 'int':
|
||||||
|
$value = (int)$value;
|
||||||
|
break;
|
||||||
|
case 'select':
|
||||||
|
$value = explode(',', $value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$fields[$name] = array_filter([
|
||||||
|
'value' => $value,
|
||||||
|
'type' => $data['type'],
|
||||||
|
'label' => $data['label'],
|
||||||
|
'values' => $data['values'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse custom fields
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected static function parseCustomfields(array $cfs)
|
||||||
|
{
|
||||||
|
$contact = [];
|
||||||
|
$definitions = Api\Storage\Customfields::get(self::APP);
|
||||||
|
|
||||||
|
foreach($definitions as $name => $definition)
|
||||||
|
{
|
||||||
|
$data = $cfs[$name];
|
||||||
|
if (isset($data))
|
||||||
|
{
|
||||||
|
if (is_scalar($data) || is_array($data) && !isset($data['value']))
|
||||||
|
{
|
||||||
|
$data = ['value' => $data];
|
||||||
|
}
|
||||||
|
if (!is_array($data) || !array_key_exists('value', $data))
|
||||||
|
{
|
||||||
|
throw new \InvalidArgumentException("Invalid customfield object $name: ".json_encode($data, self::JSON_OPTIONS_ERROR));
|
||||||
|
}
|
||||||
|
switch($definition['type'])
|
||||||
|
{
|
||||||
|
case 'date-time':
|
||||||
|
$data['value'] = Api\DateTime::to($data['value'], 'object');
|
||||||
|
break;
|
||||||
|
case 'float':
|
||||||
|
$data['value'] = (double)$data['value'];
|
||||||
|
break;
|
||||||
|
case 'int':
|
||||||
|
$data['value'] = round($data['value']);
|
||||||
|
break;
|
||||||
|
case 'select':
|
||||||
|
if (is_scalar($data['value'])) $data['value'] = explode(',', $data['value']);
|
||||||
|
$data['value'] = array_intersect(array_keys($definition['values']), $data['value']);
|
||||||
|
$data['value'] = $data['value'] ? implode(',', (array)$data['value']) : null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$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 object of category-name(s) => true
|
||||||
|
*
|
||||||
|
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-2.5.4
|
||||||
|
* @param ?string $cat_ids comma-sep. cat_id's
|
||||||
|
* @return true[]
|
||||||
|
*/
|
||||||
|
protected static function categories(?string $cat_ids)
|
||||||
|
{
|
||||||
|
$cat_ids = array_filter($cat_ids ? explode(',', $cat_ids): []);
|
||||||
|
|
||||||
|
return array_combine(array_map(static function ($cat_id)
|
||||||
|
{
|
||||||
|
return Api\Categories::id2name($cat_id);
|
||||||
|
}, $cat_ids), array_fill(0, count($cat_ids), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse categories object
|
||||||
|
*
|
||||||
|
* @param array $categories category-name => true pairs
|
||||||
|
* @return ?string comma-separated cat_id's
|
||||||
|
* @todo make that generic, so JsContact & JSCalendar have not to overwrite it
|
||||||
|
*/
|
||||||
|
protected static function parseCategories(array $categories, bool $multiple=true)
|
||||||
|
{
|
||||||
|
static $bo=null;
|
||||||
|
$cat_ids = [];
|
||||||
|
if ($categories)
|
||||||
|
{
|
||||||
|
if (count($categories) > 1 && !$multiple)
|
||||||
|
{
|
||||||
|
throw new JsParseException("Only a single category is supported!");
|
||||||
|
}
|
||||||
|
if (!isset($bo)) $bo = new Api\Categories($GLOBALS['egw_info']['user']['account_id'], static::APP);
|
||||||
|
foreach($categories as $name => $true)
|
||||||
|
{
|
||||||
|
if (!($cat_id = $bo->name2id($name)))
|
||||||
|
{
|
||||||
|
$cat_id = $bo->add(array('name' => $name, 'descr' => $name, 'access' => 'private'));
|
||||||
|
}
|
||||||
|
$cat_ids[] = $cat_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $cat_ids ? implode(',', $cat_ids) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch JsCard
|
||||||
|
*
|
||||||
|
* @param array $patches JSON path
|
||||||
|
* @param array $jscard to patch
|
||||||
|
* @param bool $create =false true: create missing components
|
||||||
|
* @return array patched $jscard
|
||||||
|
*/
|
||||||
|
public static function patch(array $patches, array $jscard, bool $create=false)
|
||||||
|
{
|
||||||
|
foreach($patches as $path => $value)
|
||||||
|
{
|
||||||
|
$parts = explode('/', $path);
|
||||||
|
$target = &$jscard;
|
||||||
|
foreach($parts as $n => $part)
|
||||||
|
{
|
||||||
|
if (!isset($target[$part]) && $n < count($parts)-1 && !$create)
|
||||||
|
{
|
||||||
|
throw new \InvalidArgumentException("Trying to patch not existing attribute with path $path!");
|
||||||
|
}
|
||||||
|
$parent = $target;
|
||||||
|
$target = &$target[$part];
|
||||||
|
}
|
||||||
|
if (isset($value))
|
||||||
|
{
|
||||||
|
$target = $value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unset($parent[$part]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $jscard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an integer
|
||||||
|
*
|
||||||
|
* @param int $value
|
||||||
|
* @return int
|
||||||
|
* @throws \TypeError
|
||||||
|
*/
|
||||||
|
public static function parseInt(int $value)
|
||||||
|
{
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an float value
|
||||||
|
*
|
||||||
|
* @param float $value
|
||||||
|
* @return float
|
||||||
|
* @throws \TypeError
|
||||||
|
*/
|
||||||
|
public static function parseFloat(float $value)
|
||||||
|
{
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map all kind of exceptions while parsing to a JsCalendarParseException
|
||||||
|
*
|
||||||
|
* @param \Throwable $e
|
||||||
|
* @param string $type
|
||||||
|
* @param ?string $name
|
||||||
|
* @param mixed $value
|
||||||
|
* @throws JsParseException
|
||||||
|
*/
|
||||||
|
protected static function handleExceptions(\Throwable $e, $type='JsCalendar', ?string $name, $value)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
catch (\JsonException $e) {
|
||||||
|
throw new JsParseException("Error parsing JSON: ".$e->getMessage(), 422, $e);
|
||||||
|
}
|
||||||
|
catch (\InvalidArgumentException $e) {
|
||||||
|
throw new JsParseException("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 JsParseException("Error parsing $type attribute '$name': $message", 422, $e);
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
throw new JsParseException("Error parsing $type attribute '$name': ". $e->getMessage(), 422, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,12 +19,13 @@ use EGroupware\Api;
|
|||||||
* @link https://datatracker.ietf.org/doc/html/rfc8984
|
* @link https://datatracker.ietf.org/doc/html/rfc8984
|
||||||
* @link https://jmap.io/spec-calendars.html
|
* @link https://jmap.io/spec-calendars.html
|
||||||
*/
|
*/
|
||||||
class JsCalendar
|
class JsCalendar extends JsBase
|
||||||
{
|
{
|
||||||
|
const APP = 'calendar';
|
||||||
|
|
||||||
const MIME_TYPE = "application/jscalendar+json";
|
const MIME_TYPE = "application/jscalendar+json";
|
||||||
const MIME_TYPE_JSEVENT = "application/jscalendar+json;type=event";
|
const MIME_TYPE_JSEVENT = "application/jscalendar+json;type=event";
|
||||||
const MIME_TYPE_JSTASK = "application/jscalendar+json;type=task";
|
const MIME_TYPE_JSTASK = "application/jscalendar+json;type=task";
|
||||||
const MIME_TYPE_JSON = "application/json";
|
|
||||||
|
|
||||||
const TYPE_EVENT = 'Event';
|
const TYPE_EVENT = 'Event';
|
||||||
|
|
||||||
@ -200,174 +201,6 @@ class JsCalendar
|
|||||||
return $event;
|
return $event;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 string|null $old old value, if given it must NOT change
|
|
||||||
* @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, string $old=null, bool $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();
|
|
||||||
}
|
|
||||||
if (strpos($uid, self::URN_UUID_PREFIX) === 0)
|
|
||||||
{
|
|
||||||
$uid = substr($uid, strlen(self::URN_UUID_PREFIX));
|
|
||||||
}
|
|
||||||
if (isset($old) && $old !== $uid)
|
|
||||||
{
|
|
||||||
throw new \InvalidArgumentException("You must NOT change the UID ('$old'): ".json_encode($uid));
|
|
||||||
}
|
|
||||||
return $uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON options for errors thrown as exceptions
|
|
||||||
*/
|
|
||||||
const JSON_OPTIONS_ERROR = JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE;
|
|
||||||
|
|
||||||
const AT_TYPE = '@type';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return EGroupware custom fields
|
|
||||||
*
|
|
||||||
* @param array $contact
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected static function customfields(array $contact)
|
|
||||||
{
|
|
||||||
$fields = [];
|
|
||||||
foreach(Api\Storage\Customfields::get('calendar') as $name => $data)
|
|
||||||
{
|
|
||||||
$value = $contact['#'.$name];
|
|
||||||
if (isset($value))
|
|
||||||
{
|
|
||||||
switch($data['type'])
|
|
||||||
{
|
|
||||||
case 'date-time':
|
|
||||||
$value = Api\DateTime::to($value, Api\DateTime::RFC3339);
|
|
||||||
break;
|
|
||||||
case 'float':
|
|
||||||
$value = (double)$value;
|
|
||||||
break;
|
|
||||||
case 'int':
|
|
||||||
$value = (int)$value;
|
|
||||||
break;
|
|
||||||
case 'select':
|
|
||||||
$value = explode(',', $value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$fields[$name] = array_filter([
|
|
||||||
'value' => $value,
|
|
||||||
'type' => $data['type'],
|
|
||||||
'label' => $data['label'],
|
|
||||||
'values' => $data['values'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse custom fields
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected static function parseCustomfields(array $cfs)
|
|
||||||
{
|
|
||||||
$contact = [];
|
|
||||||
$definitions = Api\Storage\Customfields::get('calendar');
|
|
||||||
|
|
||||||
foreach($definitions as $name => $definition)
|
|
||||||
{
|
|
||||||
$data = $cfs[$name];
|
|
||||||
if (isset($data))
|
|
||||||
{
|
|
||||||
if (is_scalar($data))
|
|
||||||
{
|
|
||||||
$data = ['value' => $data];
|
|
||||||
}
|
|
||||||
if (!is_array($data) || !array_key_exists('value', $data))
|
|
||||||
{
|
|
||||||
throw new \InvalidArgumentException("Invalid customfield object $name: ".json_encode($data, self::JSON_OPTIONS_ERROR));
|
|
||||||
}
|
|
||||||
switch($definition['type'])
|
|
||||||
{
|
|
||||||
case 'date-time':
|
|
||||||
$data['value'] = Api\DateTime::to($data['value'], 'object');
|
|
||||||
break;
|
|
||||||
case 'float':
|
|
||||||
$data['value'] = (double)$data['value'];
|
|
||||||
break;
|
|
||||||
case 'int':
|
|
||||||
$data['value'] = round($data['value']);
|
|
||||||
break;
|
|
||||||
case 'select':
|
|
||||||
if (is_scalar($data['value'])) $data['value'] = explode(',', $data['value']);
|
|
||||||
$data['value'] = array_intersect(array_keys($definition['values']), $data['value']);
|
|
||||||
$data['value'] = $data['value'] ? implode(',', (array)$data['value']) : null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$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 object of category-name(s) => true
|
|
||||||
*
|
|
||||||
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-2.5.4
|
|
||||||
* @param ?string $cat_ids comma-sep. cat_id's
|
|
||||||
* @return true[]
|
|
||||||
*/
|
|
||||||
protected static function categories(?string $cat_ids)
|
|
||||||
{
|
|
||||||
$cat_ids = array_filter($cat_ids ? explode(',', $cat_ids): []);
|
|
||||||
|
|
||||||
return array_combine(array_map(static function ($cat_id)
|
|
||||||
{
|
|
||||||
return Api\Categories::id2name($cat_id);
|
|
||||||
}, $cat_ids), array_fill(0, count($cat_ids), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse categories object
|
* Parse categories object
|
||||||
*
|
*
|
||||||
@ -421,29 +254,6 @@ class JsCalendar
|
|||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a date-time value in UTC
|
|
||||||
*
|
|
||||||
* @link https://datatracker.ietf.org/doc/html/rfc8984#section-1.4.4
|
|
||||||
* @param null|string|\DateTime $date
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
protected static function UTCDateTime($date)
|
|
||||||
{
|
|
||||||
static $utc=null;
|
|
||||||
if (!isset($utc)) $utc = new \DateTimeZone('UTC');
|
|
||||||
|
|
||||||
if (!isset($date))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$date = Api\DateTime::to($date, 'object');
|
|
||||||
$date->setTimezone($utc);
|
|
||||||
|
|
||||||
// we need to use "Z", not "+00:00"
|
|
||||||
return substr($date->format(Api\DateTime::RFC3339), 0, -6).'Z';
|
|
||||||
}
|
|
||||||
|
|
||||||
const DATETIME_FORMAT = 'Y-m-d\TH:i:s';
|
const DATETIME_FORMAT = 'Y-m-d\TH:i:s';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -874,76 +684,6 @@ class JsCalendar
|
|||||||
return $alerts;
|
return $alerts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch JsEvent
|
|
||||||
*
|
|
||||||
* @param array $patches JSON path
|
|
||||||
* @param array $jsevent to patch
|
|
||||||
* @param bool $create =false true: create missing components
|
|
||||||
* @return array patched $jsevent
|
|
||||||
*/
|
|
||||||
public static function patch(array $patches, array $jsevent, bool $create=false)
|
|
||||||
{
|
|
||||||
foreach($patches as $path => $value)
|
|
||||||
{
|
|
||||||
$parts = explode('/', $path);
|
|
||||||
$target = &$jsevent;
|
|
||||||
foreach($parts as $n => $part)
|
|
||||||
{
|
|
||||||
if (!isset($target[$part]) && $n < count($parts)-1 && !$create)
|
|
||||||
{
|
|
||||||
throw new \InvalidArgumentException("Trying to patch not existing attribute with path $path!");
|
|
||||||
}
|
|
||||||
$parent = $target;
|
|
||||||
$target = &$target[$part];
|
|
||||||
}
|
|
||||||
if (isset($value))
|
|
||||||
{
|
|
||||||
$target = $value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
unset($parent[$part]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $jsevent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map all kind of exceptions while parsing to a JsCalendarParseException
|
|
||||||
*
|
|
||||||
* @param \Throwable $e
|
|
||||||
* @param string $type
|
|
||||||
* @param ?string $name
|
|
||||||
* @param mixed $value
|
|
||||||
* @throws JsCalendarParseException
|
|
||||||
*/
|
|
||||||
protected static function handleExceptions(\Throwable $e, $type='JsCalendar', ?string $name, $value)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
catch (\JsonException $e) {
|
|
||||||
throw new JsCalendarParseException("Error parsing JSON: ".$e->getMessage(), 422, $e);
|
|
||||||
}
|
|
||||||
catch (\InvalidArgumentException $e) {
|
|
||||||
throw new JsCalendarParseException("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 JsCalendarParseException("Error parsing $type attribute '$name': $message", 422, $e);
|
|
||||||
}
|
|
||||||
catch (\Throwable $e) {
|
|
||||||
throw new JsCalendarParseException("Error parsing $type attribute '$name': ". $e->getMessage(), 422, $e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return \calendar_boupdate
|
* @return \calendar_boupdate
|
||||||
*/
|
*/
|
||||||
|
@ -18,7 +18,7 @@ use Throwable;
|
|||||||
*
|
*
|
||||||
* @link * @link https://datatracker.ietf.org/doc/html/rfc8984
|
* @link * @link https://datatracker.ietf.org/doc/html/rfc8984
|
||||||
*/
|
*/
|
||||||
class JsCalendarParseException extends \InvalidArgumentException
|
class JsParseException extends \InvalidArgumentException
|
||||||
{
|
{
|
||||||
public function __construct($message = "", $code = 422, Throwable $previous = null)
|
public function __construct($message = "", $code = 422, Throwable $previous = null)
|
||||||
{
|
{
|
@ -19,12 +19,13 @@ use EGroupware\Api;
|
|||||||
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07 (newer, here implemented format)
|
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07 (newer, here implemented format)
|
||||||
* @link https://datatracker.ietf.org/doc/html/rfc7095 jCard (older vCard compatible contact data as JSON, NOT implemented here!)
|
* @link https://datatracker.ietf.org/doc/html/rfc7095 jCard (older vCard compatible contact data as JSON, NOT implemented here!)
|
||||||
*/
|
*/
|
||||||
class JsContact
|
class JsContact extends Api\CalDAV\JsBase
|
||||||
{
|
{
|
||||||
|
const APP = 'addressbook';
|
||||||
|
|
||||||
const MIME_TYPE = "application/jscontact+json";
|
const MIME_TYPE = "application/jscontact+json";
|
||||||
const MIME_TYPE_JSCARD = "application/jscontact+json;type=card";
|
const MIME_TYPE_JSCARD = "application/jscontact+json;type=card";
|
||||||
const MIME_TYPE_JSCARDGROUP = "application/jscontact+json;type=cardgroup";
|
const MIME_TYPE_JSCARDGROUP = "application/jscontact+json;type=cardgroup";
|
||||||
const MIME_TYPE_JSON = "application/json";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get jsCard for given contact
|
* Get jsCard for given contact
|
||||||
@ -216,50 +217,6 @@ class JsContact
|
|||||||
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 string|null $old old value, if given it must NOT change
|
|
||||||
* @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, string $old=null, bool $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();
|
|
||||||
}
|
|
||||||
if (strpos($uid, self::URN_UUID_PREFIX) === 0)
|
|
||||||
{
|
|
||||||
$uid = substr($uid, strlen(self::URN_UUID_PREFIX));
|
|
||||||
}
|
|
||||||
if (isset($old) && $old !== $uid)
|
|
||||||
{
|
|
||||||
throw new \InvalidArgumentException("You must NOT change the UID ('$old'): ".json_encode($uid));
|
|
||||||
}
|
|
||||||
return $uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON options for errors thrown as exceptions
|
* JSON options for errors thrown as exceptions
|
||||||
*/
|
*/
|
||||||
@ -390,123 +347,6 @@ class JsContact
|
|||||||
return $contact;
|
return $contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return EGroupware custom fields
|
|
||||||
*
|
|
||||||
* @param array $contact
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected static function customfields(array $contact)
|
|
||||||
{
|
|
||||||
$fields = [];
|
|
||||||
foreach(Api\Storage\Customfields::get('addressbook') as $name => $data)
|
|
||||||
{
|
|
||||||
$value = $contact['#'.$name];
|
|
||||||
if (isset($value))
|
|
||||||
{
|
|
||||||
switch($data['type'])
|
|
||||||
{
|
|
||||||
case 'date-time':
|
|
||||||
$value = Api\DateTime::to($value, Api\DateTime::RFC3339);
|
|
||||||
break;
|
|
||||||
case 'float':
|
|
||||||
$value = (double)$value;
|
|
||||||
break;
|
|
||||||
case 'int':
|
|
||||||
$value = (int)$value;
|
|
||||||
break;
|
|
||||||
case 'select':
|
|
||||||
$value = explode(',', $value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$fields[$name] = array_filter([
|
|
||||||
'value' => $value,
|
|
||||||
'type' => $data['type'],
|
|
||||||
'label' => $data['label'],
|
|
||||||
'values' => $data['values'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse custom fields
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected static function parseCustomfields(array $cfs)
|
|
||||||
{
|
|
||||||
$contact = [];
|
|
||||||
$definitions = Api\Storage\Customfields::get('addressbook');
|
|
||||||
|
|
||||||
foreach($definitions as $name => $definition)
|
|
||||||
{
|
|
||||||
$data = $cfs[$name];
|
|
||||||
if (isset($data))
|
|
||||||
{
|
|
||||||
if (is_scalar($data))
|
|
||||||
{
|
|
||||||
$data = ['value' => $data];
|
|
||||||
}
|
|
||||||
if (!is_array($data) || !array_key_exists('value', $data))
|
|
||||||
{
|
|
||||||
throw new \InvalidArgumentException("Invalid customfield object $name: ".json_encode($data, self::JSON_OPTIONS_ERROR));
|
|
||||||
}
|
|
||||||
switch($definition['type'])
|
|
||||||
{
|
|
||||||
case 'date-time':
|
|
||||||
$data['value'] = Api\DateTime::to($data['value'], 'object');
|
|
||||||
break;
|
|
||||||
case 'float':
|
|
||||||
$data['value'] = (double)$data['value'];
|
|
||||||
break;
|
|
||||||
case 'int':
|
|
||||||
$data['value'] = round($data['value']);
|
|
||||||
break;
|
|
||||||
case 'select':
|
|
||||||
if (is_scalar($data['value'])) $data['value'] = explode(',', $data['value']);
|
|
||||||
$data['value'] = array_intersect(array_keys($definition['values']), $data['value']);
|
|
||||||
$data['value'] = $data['value'] ? implode(',', (array)$data['value']) : null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$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 object of category-name(s) => true
|
|
||||||
*
|
|
||||||
* @link https://datatracker.ietf.org/doc/html/draft-ietf-jmap-jscontact-07#section-2.5.4
|
|
||||||
* @param ?string $cat_ids comma-sep. cat_id's
|
|
||||||
* @return true[]
|
|
||||||
*/
|
|
||||||
protected static function categories(?string $cat_ids)
|
|
||||||
{
|
|
||||||
$cat_ids = array_filter($cat_ids ? explode(',', $cat_ids): []);
|
|
||||||
|
|
||||||
return array_combine(array_map(static function ($cat_id)
|
|
||||||
{
|
|
||||||
return Api\Categories::id2name($cat_id);
|
|
||||||
}, $cat_ids), array_fill(0, count($cat_ids), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse categories object
|
* Parse categories object
|
||||||
*
|
*
|
||||||
@ -1359,76 +1199,6 @@ class JsContact
|
|||||||
return $members;
|
return $members;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch JsCard
|
|
||||||
*
|
|
||||||
* @param array $patches JSON path
|
|
||||||
* @param array $jscard to patch
|
|
||||||
* @param bool $create =false true: create missing components
|
|
||||||
* @return array patched $jscard
|
|
||||||
*/
|
|
||||||
public static function patch(array $patches, array $jscard, bool $create=false)
|
|
||||||
{
|
|
||||||
foreach($patches as $path => $value)
|
|
||||||
{
|
|
||||||
$parts = explode('/', $path);
|
|
||||||
$target = &$jscard;
|
|
||||||
foreach($parts as $n => $part)
|
|
||||||
{
|
|
||||||
if (!isset($target[$part]) && $n < count($parts)-1 && !$create)
|
|
||||||
{
|
|
||||||
throw new \InvalidArgumentException("Trying to patch not existing attribute with path $path!");
|
|
||||||
}
|
|
||||||
$parent = $target;
|
|
||||||
$target = &$target[$part];
|
|
||||||
}
|
|
||||||
if (isset($value))
|
|
||||||
{
|
|
||||||
$target = $value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
unset($parent[$part]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $jscard;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
<?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);
|
|
||||||
}
|
|
||||||
}
|
|
1052
timesheet/src/ApiHandler.php
Normal file
1052
timesheet/src/ApiHandler.php
Normal file
File diff suppressed because it is too large
Load Diff
184
timesheet/src/JsTimesheet.php
Normal file
184
timesheet/src/JsTimesheet.php
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* EGroupware Timesheet - JsTimesheet
|
||||||
|
*
|
||||||
|
* @link https://www.egroupware.org
|
||||||
|
* @author Ralf Becker <rb@egroupware.org>
|
||||||
|
* @package calendar
|
||||||
|
* @copyright (c) 2023 by Ralf Becker <rb@egroupware.org>
|
||||||
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace EGroupware\Timesheet;
|
||||||
|
|
||||||
|
use EGroupware\Api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendering events as JSON using new JsCalendar format
|
||||||
|
*
|
||||||
|
* @link https://datatracker.ietf.org/doc/html/rfc8984
|
||||||
|
* @link https://jmap.io/spec-calendars.html
|
||||||
|
*/
|
||||||
|
class JsTimesheet extends Api\CalDAV\JsBase
|
||||||
|
{
|
||||||
|
const APP = 'timesheet';
|
||||||
|
|
||||||
|
const TYPE_TIMESHEET = 'timesheet';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get JsEvent for given event
|
||||||
|
*
|
||||||
|
* @param int|array $event
|
||||||
|
* @param bool|"pretty" $encode true: JSON encode, "pretty": JSON encode with pretty-print, false: return raw data e.g. from listing
|
||||||
|
* @param ?array $exceptions=null
|
||||||
|
* @return string|array
|
||||||
|
* @throws Api\Exception\NotFound
|
||||||
|
*/
|
||||||
|
public static function JsTimesheet(array $timesheet, $encode=true, array $exceptions=[])
|
||||||
|
{
|
||||||
|
static $bo = null;
|
||||||
|
if (!isset($bo)) $bo = new \timesheet_bo();
|
||||||
|
|
||||||
|
if (isset($timesheet['ts_id']))
|
||||||
|
{
|
||||||
|
$timesheet = Api\Db::strip_array_keys($timesheet, 'ts_');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array_filter([
|
||||||
|
self::AT_TYPE => self::TYPE_TIMESHEET,
|
||||||
|
//'uid' => self::uid($timesheet['uid']),
|
||||||
|
'id' => (int)$timesheet['id'],
|
||||||
|
'title' => $timesheet['title'],
|
||||||
|
'description' => $timesheet['description'],
|
||||||
|
'start' => self::UTCDateTime($timesheet['start'], true),
|
||||||
|
'duration' => (int)$timesheet['duration'],
|
||||||
|
'quantity' => (double)$timesheet['quantity'],
|
||||||
|
'unitprice' => (double)$timesheet['unitprice'],
|
||||||
|
'category' => self::categories($timesheet['cat_id']),
|
||||||
|
'owner' => self::account($timesheet['owner']),
|
||||||
|
'created' => self::UTCDateTime($timesheet['created'], true),
|
||||||
|
'modified' => self::UTCDateTime($timesheet['modified'], true),
|
||||||
|
'modifier' => self::account($timesheet['modifier']),
|
||||||
|
'pricelist' => (int)$timesheet['pl_id'] ?: null,
|
||||||
|
'status' => $bo->status_labels[$timesheet['status']] ?? null,
|
||||||
|
'egroupware.org:customfields' => self::customfields($timesheet),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($encode)
|
||||||
|
{
|
||||||
|
return Api\CalDAV::json_encode($data, $encode === "pretty");
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse JsTimesheet
|
||||||
|
*
|
||||||
|
* @param string $json
|
||||||
|
* @param array $old=[] existing contact for patch
|
||||||
|
* @param ?string $content_type=null application/json no strict parsing and automatic patch detection, if method not 'PATCH' or 'PUT'
|
||||||
|
* @param string $method='PUT' 'PUT', 'POST' or 'PATCH'
|
||||||
|
* @return array with "ts_" prefix
|
||||||
|
*/
|
||||||
|
public static function parseJsTimesheet(string $json, array $old=[], string $content_type=null, $method='PUT')
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$data = json_decode($json, true, 10, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
// check if we use patch: method is PATCH or method is POST AND keys contain slashes
|
||||||
|
if ($method === 'PATCH')
|
||||||
|
{
|
||||||
|
// apply patch on JsCard of contact
|
||||||
|
$data = self::patch($data, $old ? self::getJsCalendar($old, false) : [], !$old);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist
|
||||||
|
|
||||||
|
// check required fields
|
||||||
|
if (!$old || !$method === 'PATCH')
|
||||||
|
{
|
||||||
|
static $required = ['title', 'start', 'duration'];
|
||||||
|
if (($missing = array_diff_key(array_filter(array_intersect_key($data), array_flip($required)), array_flip($required))))
|
||||||
|
{
|
||||||
|
throw new Api\CalDAV\JsParseException("Required field(s) ".implode(', ', $missing)." missing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$timesheet = $method === 'PATCH' ? $old : [];
|
||||||
|
foreach ($data as $name => $value)
|
||||||
|
{
|
||||||
|
switch ($name)
|
||||||
|
{
|
||||||
|
case 'title':
|
||||||
|
case 'description':
|
||||||
|
$timesheet['ts_'.$name] = $value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'start':
|
||||||
|
$timesheet['ts_start'] = Api\DateTime::server2user($value, 'ts');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'duration':
|
||||||
|
$timesheet['ts_duration'] = self::parseInt($value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'quantity':
|
||||||
|
case 'unitprice':
|
||||||
|
$timesheet['ts_'.$name] = self::parseFloat($value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'owner':
|
||||||
|
$timesheet['ts_owner'] = self::parseAccount($value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'category':
|
||||||
|
$timesheet['cat_id'] = self::parseCategories($value, false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
$timesheet['ts_status'] = self::parseStatus($value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'egroupware.org:customfields':
|
||||||
|
$timesheet += self::parseCustomfields($value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'prodId':
|
||||||
|
case 'created':
|
||||||
|
case 'modified':
|
||||||
|
case 'modifier':
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
error_log(__METHOD__ . "() $name=" . json_encode($value, self::JSON_OPTIONS_ERROR) . ' --> ignored');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
self::handleExceptions($e, 'JsTimesheet', $name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $timesheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a status label into it's numerical ID
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return int|null
|
||||||
|
* @throws Api\CalDAV\JsParseException
|
||||||
|
*/
|
||||||
|
protected static function parseStatus(string $value)
|
||||||
|
{
|
||||||
|
static $bo=null;
|
||||||
|
if (!isset($bo)) $bo = new \timesheet_bo();
|
||||||
|
|
||||||
|
if (($status_id = array_search($value, $bo->status_labels)) === false)
|
||||||
|
{
|
||||||
|
throw new Api\CalDAV\JsParseException("Invalid status value '$value', allowed '".implode("', '", $bo->status_labels)."'");
|
||||||
|
}
|
||||||
|
return $status_id;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user