WIP InfoLog REST API

This commit is contained in:
ralf 2024-05-06 12:20:41 +02:00
parent ffca28dd1d
commit 636cb10cab
2 changed files with 205 additions and 40 deletions

View File

@ -29,6 +29,7 @@ class JsCalendar extends JsBase
const TYPE_EVENT = 'Event'; const TYPE_EVENT = 'Event';
const TYPE_TASK = 'Task'; const TYPE_TASK = 'Task';
const TYPE_RELATION = 'Relation';
/** /**
* Get JsEvent for given event * Get JsEvent for given event
@ -158,7 +159,7 @@ class JsCalendar extends JsBase
break; break;
case 'privacy': case 'privacy':
$event['public'] = $value !== 'private'; $event['public'] = self::parsePrivacy($value, true);
break; break;
case 'alerts': case 'alerts':
@ -238,8 +239,6 @@ class JsCalendar extends JsBase
self::Duration(0, $entry['info_used_time']*60) : null, self::Duration(0, $entry['info_used_time']*60) : null,
'estimatedDuration' => $entry['info_plannedtime'] ? 'estimatedDuration' => $entry['info_plannedtime'] ?
self::Duration(0, $entry['info_plannedtime']*60) : null, self::Duration(0, $entry['info_plannedtime']*60) : null,
'recurrenceRules' => isset($entry['#RRULE']) ? null : null,
'recurrenceOverrides' => null,
//'freeBusyStatus' => $entry['non_blocking'] ? 'free' : null, // default is busy //'freeBusyStatus' => $entry['non_blocking'] ? 'free' : null, // default is busy
'description' => $entry['info_des'], 'description' => $entry['info_des'],
'participants' => self::Responsible($entry), 'participants' => self::Responsible($entry),
@ -252,10 +251,13 @@ class JsCalendar extends JsBase
'privacy' => $entry['info_access'], 'privacy' => $entry['info_access'],
'percentComplete' => (int)$entry['info_percent'], 'percentComplete' => (int)$entry['info_percent'],
'egroupware.org:type' => $entry['info_type'], 'egroupware.org:type' => $entry['info_type'],
'egroupware.org:pricelist' => $entry['pl_id'] ? (int)$entry['pl_id'] : null,
'egroupware.org:price' => $entry['info_price'] ? (double)$entry['info_price'] : null,
'egroupware.org:completed' => $entry['info_datecomplete'] ? 'egroupware.org:completed' => $entry['info_datecomplete'] ?
self::DateTime($entry['info_datecompleted'], Api\DateTime::$user_timezone->getName()) : null, self::DateTime($entry['info_datecompleted'], Api\DateTime::$user_timezone->getName()) : null,
] + self::Locations(['location' => $entry['info_location'] ?? null]) + self::relatedToParent($entry['info_id_parent']) + [
'egroupware.org:customfields' => self::customfields($entry, 'infolog'), 'egroupware.org:customfields' => self::customfields($entry, 'infolog'),
] + self::Locations(['location' => $entry['info_location'] ?? null]); ];
if (!empty($entry['##RRULE'])) if (!empty($entry['##RRULE']))
{ {
@ -322,25 +324,35 @@ class JsCalendar extends JsBase
break; break;
case 'start': case 'start':
$event['info_startdate'] = self::parseDateTime($value, $data['timeZone'] ?? null, $data['showWithoutTime'] ?? null);
break;
case 'due':
$event['info_enddate'] = self::parseDateTime($value, $data['timeZone'] ?? null, $data['showWithoutTime'] ?? null);
break;
case 'egroupware.org:completed':
$event['info_datecompleted'] = self::parseDateTime($value, $data['timeZone'] ?? null);
break;
case 'duration': case 'duration':
case 'timeZone': $event['info_used_time'] = self::parseSignedDuration($value, true)/60;
case 'showWithoutTime': break;
if (!isset($event['start']))
{ case 'estimatedDuration':
$event += self::parseStartDuration($data); $event['info_plannedtime'] = self::parseSignedDuration($value, true)/60;
}
break; break;
case 'participants': case 'participants':
$event += self::parseParticipants($value, $strict, $calendar_owner); $event += self::parseResponsible($value, $strict, $calendar_owner);
break; break;
case 'priority': case 'priority':
$event['priority'] = self::parsePriority($value); $event['info_priority'] = self::parsePriority($value, true);
break; break;
case 'privacy': case 'privacy':
$event['info_public'] = $value; $event['info_access'] = self::parsePrivacy($value);
break; break;
case 'recurrenceRules': case 'recurrenceRules':
@ -348,23 +360,30 @@ class JsCalendar extends JsBase
break; break;
case 'categories': case 'categories':
$event['info_cat'] = (int)self::parseCategories($value); $event['info_cat'] = (int)self::parseCategories($value, false);
break;
case 'relatedTo':
$event['info_id_parent'] = self::parseRelatedToParent($value);
break; break;
case 'egroupware.org:customfields': case 'egroupware.org:customfields':
$event = array_merge($event, self::parseCustomfields($value, $strict)); $event = array_merge($event, self::parseCustomfields($value, 'infolog'));
break;
case 'egroupware.org:completed':
$event['info_datecomplete'] = self::parseDateTime();
break; break;
case 'egroupware.org:type': case 'egroupware.org:type':
$event['info_type'] = $value; $event['info_type'] = self::parseInfoType($value);
break;
case 'egroupware.org:price':
$event['info_price'] = self::parseFloat($value);
break;
case 'prodId': case 'prodId':
case 'created': case 'created':
case 'updated': case 'updated':
case 'showWithoutTime':
case 'timeZone':
break; break;
default: default:
@ -433,17 +452,20 @@ class JsCalendar extends JsBase
* Parse categories object * Parse categories object
* *
* @param array $categories category-name => true pairs * @param array $categories category-name => true pairs
* @param bool $multiple * @param bool $calendar true (default) use calendar AND support multiple categories, false: InfoLog with only one Category
* @return ?string comma-separated cat_id's * @return ?string comma-separated cat_id's
*/ */
protected static function parseCategories(array $categories, bool $multiple=true) protected static function parseCategories(array $categories, bool $calendar=true)
{ {
static $bo=null;
$cat_ids = []; $cat_ids = [];
if ($categories) if ($categories)
{ {
if (!isset($bo)) $bo = new \calendar_boupdate(); $cat_ids = ($calendar ? self::getCalendar() : self::getInfolog())->find_or_add_categories(array_keys($categories));
$cat_ids = $bo->find_or_add_categories(array_keys($categories));
if (!$calendar && count ($cat_ids) > 1)
{
throw new \InvalidArgumentException("InfoLog supports only a single category currently!");
}
} }
return $cat_ids ? implode(',', $cat_ids) : null; return $cat_ids ? implode(',', $cat_ids) : null;
} }
@ -509,20 +531,27 @@ class JsCalendar extends JsBase
} }
/** /**
* Return a duration calculated from given start- and end-time * Return a duration calculated from given start- and end-time or a duration in seconds (start=0)
* *
* @link https://datatracker.ietf.org/doc/html/rfc8984#name-duration * @link https://datatracker.ietf.org/doc/html/rfc8984#name-duration
* @param int|string|\DateTime $start * @param int|string|\DateTime $start start-time or 0 to use duration in $end
* @param int|string|\DateTime $end * @param int|string|\DateTime $end end-time or duration for $start===0
* @param bool $whole_day * @param bool $whole_day true: handling for whole-day events, default: false
* @return string * @return string
*/ */
protected static function Duration($start, $end, bool $whole_day=false) protected static function Duration($start, $end, bool $whole_day=false)
{ {
$start = Api\DateTime::to($start, 'object'); if (!$start && is_numeric($end))
$end = Api\DateTime::to($end, 'object'); {
$value = $end;
}
else
{
$start = Api\DateTime::to($start, 'object');
$end = Api\DateTime::to($end, 'object');
$value = $end->getTimestamp() - $start->getTimestamp() + (int)$whole_day; $value = $end->getTimestamp() - $start->getTimestamp() + (int)$whole_day;
}
$duration = ''; $duration = '';
if ($value < 0) if ($value < 0)
@ -548,6 +577,20 @@ class JsCalendar extends JsBase
return $duration; return $duration;
} }
/**
* Parse a DateTime value
*
* @param string $value
* @param string|null $timezone
* @param bool $showWithoutTime true: return H:i set to 00:00
* @return Api\DateTime
* @throws Api\Exception
*/
protected static function parseDateTime(string $value, ?string $timezone=null, bool $showWithoutTime=false)
{
return new Api\DateTime($value, !empty($timezone) ? new \DateTimeZone($timezone) : null);
}
protected static function parseStartDuration(array $data) protected static function parseStartDuration(array $data)
{ {
$parsed = []; $parsed = [];
@ -556,8 +599,7 @@ class JsCalendar extends JsBase
{ {
throw new \InvalidArgumentException("Invalid or missing start: ".json_encode($data['start'])); throw new \InvalidArgumentException("Invalid or missing start: ".json_encode($data['start']));
} }
$parsed['start'] = new Api\DateTime($data['start'], !empty($data['timeZone']) ? new \DateTimeZone($data['timeZone']) : null); $parsed['start'] = self::parseDateTime($data['start'], $parsed['tzid'] = $data['timeZone'] ?? null);
$parsed['tzid'] = $data['timeZone'] ?? null;
$duration = self::parseSignedDuration($data['duration'] ?? null); $duration = self::parseSignedDuration($data['duration'] ?? null);
$parsed['end'] = new Api\DateTime($parsed['start']); $parsed['end'] = new Api\DateTime($parsed['start']);
@ -663,10 +705,11 @@ class JsCalendar extends JsBase
* @param array $participants * @param array $participants
* @param bool $strict true: require @types and objects with attributes name, email, ... * @param bool $strict true: require @types and objects with attributes name, email, ...
* @param ?int $calendar_owner owner of the calendar / collection * @param ?int $calendar_owner owner of the calendar / collection
* @param bool $calendar true: calendar, false: infolog only supporting users and email
* @return array * @return array
* @todo Resources and Groups without email * @todo Resources and Groups without email
*/ */
protected static function parseParticipants(array $participants, bool $strict=true, int $calendar_owner=null) protected static function parseParticipants(array $participants, bool $strict=true, int $calendar_owner=null, bool $calendar=true)
{ {
$parsed = []; $parsed = [];
@ -704,7 +747,8 @@ class JsCalendar extends JsBase
'email_home' => $participant['email'], 'email_home' => $participant['email'],
], ['id','egw_addressbook.account_id as account_id','n_fn'], ], ['id','egw_addressbook.account_id as account_id','n_fn'],
'egw_addressbook.account_id IS NOT NULL DESC, n_fn IS NOT NULL DESC', 'egw_addressbook.account_id IS NOT NULL DESC, n_fn IS NOT NULL DESC',
'','',false,'OR'))) '','',false,'OR')) &&
($calendar || !empty($data['account_id'])))
{ {
// found an addressbook entry // found an addressbook entry
$uid = $data['account_id'] ? (int)$data['account_id'] : 'c'.$data['id']; $uid = $data['account_id'] ? (int)$data['account_id'] : 'c'.$data['id'];
@ -778,6 +822,38 @@ class JsCalendar extends JsBase
return $participants; return $participants;
} }
/**
* Parse participants object for InfoLog only supporting responsible and CC
*
* @param array $participants
* @param bool $strict true: require @types and objects with attributes name, email, ...
* @param ?int $calendar_owner owner of the calendar / collection
* @return array with values for info_responsible (int[]) and info_cc (comma-separated string)
*/
protected static function parseResponsible(array $participants, bool $strict=true, int $calendar_owner=null)
{
$responsible = $cc = [];
foreach(self::parseParticipants($participants, $strict, $calendar_owner, false) as $uid => $status)
{
if (is_numeric($uid))
{
// we do NOT store just owner as participant, only if he has further roles / request-participant
if ($participants[$uid]['roles'] !== ['owner' => true])
{
$responsible[] = $uid;
}
}
elseif ($uid[0] === 'e')
{
$cc[] = substr($uid, 1);
}
}
return [
'info_responsible' => $responsible,
'info_cc' => $cc ? implode(',', $cc) : null,
];
}
protected static function jscalRoles2role(array $roles=null, string $default_role=null) protected static function jscalRoles2role(array $roles=null, string $default_role=null)
{ {
$role = $default_role ?? 'REQ-PARTICIPANT'; $role = $default_role ?? 'REQ-PARTICIPANT';
@ -981,14 +1057,17 @@ class JsCalendar extends JsBase
$rrule = []; $rrule = [];
foreach($recurenaceRules as $rule) foreach($recurenaceRules as $rule)
{ {
if ($rrule)
{
throw new \InvalidArgumentException("EGroupware currently stores only a single rule!");
}
foreach($rule as $name => $value) foreach($rule as $name => $value)
{ {
$rrule[] = $name.'='.$value; $rrule[] = $name.'='.$value;
} }
break; // we support only one rule!
} }
return [ return [
'#RRULE' => $rrule ? implode(';', $rrule) : null, '##RRULE' => $rrule ? implode(';', $rrule) : null,
]; ];
} }
@ -1130,6 +1209,76 @@ class JsCalendar extends JsBase
return $alarms; return $alarms;
} }
/**
* Parse a privacy value: "public", "private" or "secret" (currently not supported by calendar or infolog)
*
* @param string $value
* @return bool
* @link https://datatracker.ietf.org/doc/html/rfc8984#name-privacy
*/
protected static function parsePrivacy(string $value, $return_bool=false)
{
if (!in_array($value, ['public', 'private'], true))
{
throw new \InvalidArgumentException("Privacy value must be either 'public' or 'private' ('secret' is currently NOT supported)!");
}
return $return_bool ? ($value === 'public') : $value;
}
protected static function relatedToParent(int $info_id_parent)
{
if (!$info_id_parent || !($parent = self::getInfolog()->read($info_id_parent)))
{
if ($info_id_parent) error_log(__METHOD__."($info_id_parent) could NOT read parent!");
return [];
}
return [
'relatedTo' => [
$parent['info_uid'] => [
self::AT_TYPE => self::TYPE_RELATION,
'relation' => 'parent',
],
],
];
}
protected static function parseRelatedToParent(array $related_to, bool $strict=false)
{
foreach($related_to as $uid => $relation)
{
if (!($parent = self::getInfolog()->read(['info_uid' => $uid])))
{
throw new \InvalidArgumentException("UID '$uid' NOT found!");
}
if ($strict && $relation[self::AT_TYPE]??null !== self::TYPE_RELATION)
{
throw new \InvalidArgumentException("Missing or invalid @Type!");
}
if ($relation['relation'] !== 'parent')
{
throw new \InvalidArgumentException("Unsupported relation-type: ".json_encode($relation['relation']??null)."!");
}
}
return $parent ? $parent['info_id'] : null;
}
/**
* Parse an InfoLog type
*
* @param string $type
* @throws \InvalidArgumentException
* @return string
*/
protected static function parseInfoType(string $type)
{
$bo = self::getInfolog();
if (!isset($bo->enums['type'][$type]))
{
throw new \InvalidArgumentException("Invalid / non-existing InfoLog type '$type', allowed values are: '".implode("', '", array_keys($bo->enums['type']))."'");
}
return $type;
}
/** /**
* @return \calendar_boupdate * @return \calendar_boupdate
*/ */

View File

@ -620,7 +620,7 @@ class infolog_groupdav extends Api\CalDAV\Handler
* @param string $prefix =null user prefix from path (eg. /ralf from /ralf/addressbook) * @param string $prefix =null user prefix from path (eg. /ralf from /ralf/addressbook)
* @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found') * @return mixed boolean true on success, false on failure or string with http status (eg. '404 Not Found')
*/ */
function put(&$options,$id,$user=null,$prefix=null) function put(&$options, $id, $user=null, $prefix=null, $method='PUT')
{ {
unset($prefix); // not used, but required by function signature unset($prefix); // not used, but required by function signature
@ -650,7 +650,23 @@ class infolog_groupdav extends Api\CalDAV\Handler
{ {
$callback_data = array(array($this, 'cat_action'), $oldTask); $callback_data = array(array($this, 'cat_action'), $oldTask);
} }
if (!($infoId = $handler->importVTODO($vTodo, $taskId, false, $user, null, $id, $callback_data))) $type = null;
if (($is_json=Api\CalDAV::isJSON($type)))
{
$task = Api\CalDAV\JsCalendar::parseJsTask($options['content'], $oldTask ?? [], $type, $method, $user) + $oldTask??[];
if ($callback_data)
{
$callback = array_shift($callback_data);
array_unshift($callback_data, $task);
$task = call_user_func_array($callback, $callback_data);
}
$infoId = $this->bo->write($task);
}
else
{
$infoId = $handler->importVTODO($vTodo, $taskId, false, $user, null, $id, $callback_data);
}
if (!$infoId)
{ {
if ($this->debug) error_log(__METHOD__."(,$id) import_vtodo($options[content]) returned false"); if ($this->debug) error_log(__METHOD__."(,$id) import_vtodo($options[content]) returned false");
return '403 Forbidden'; return '403 Forbidden';