mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-26 00:29:38 +01:00
WIP InfoLog REST API
This commit is contained in:
parent
ffca28dd1d
commit
636cb10cab
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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';
|
||||||
|
Loading…
Reference in New Issue
Block a user