2008-05-08 22:31:32 +02:00
< ? php
/**
2016-04-02 10:40:34 +02:00
* EGroupware : CalDAV / CardDAV / GroupDAV access : Calendar handler
2008-05-08 22:31:32 +02:00
*
* @ link http :// www . egroupware . org
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
* @ package calendar
2016-05-01 19:47:59 +02:00
* @ subpackage caldav
2008-05-08 22:31:32 +02:00
* @ author Ralf Becker < RalfBecker - AT - outdoor - training . de >
2016-04-02 10:40:34 +02:00
* @ copyright ( c ) 2007 - 16 by Ralf Becker < RalfBecker - AT - outdoor - training . de >
2008-05-08 22:31:32 +02:00
* @ version $Id $
*/
2016-04-02 12:44:17 +02:00
use EGroupware\Api ;
2016-05-01 19:47:59 +02:00
use EGroupware\Api\Acl ;
2016-04-02 12:44:17 +02:00
2008-05-08 22:31:32 +02:00
/**
2016-04-02 12:44:17 +02:00
* CalDAV / CardDAV / GroupDAV access : Calendar handler
2012-02-21 21:04:45 +01:00
*
2016-04-02 12:44:17 +02:00
* Permanent error_log () calls should use $this -> caldav -> log ( $str ) instead , to be send to PHP error_log ()
2012-02-21 21:04:45 +01:00
* and our request - log ( prefixed with " ### " after request and response , like exceptions ) .
2012-10-08 13:14:07 +02:00
*
2023-04-05 16:08:36 +02:00
* @ ToDo : new properties on calendars and it ' s resources specially from sharing :
2012-10-08 13:14:07 +02:00
* - for the invite property : 5.2 . 2 in https :// trac . calendarserver . org / browser / CalendarServer / trunk / doc / Extensions / caldav - sharing . txt
* - https :// trac . calendarserver . org / browser / CalendarServer / trunk / doc / Extensions / caldav - schedulingchanges . txt
2008-05-08 22:31:32 +02:00
*/
2016-04-02 12:44:17 +02:00
class calendar_groupdav extends Api\CalDAV\Handler
2008-05-08 22:31:32 +02:00
{
/**
* bo class of the application
*
2008-06-07 19:45:33 +02:00
* @ var calendar_boupdate
2008-05-08 22:31:32 +02:00
*/
var $bo ;
2010-11-08 10:25:58 +01:00
2010-10-23 13:43:52 +02:00
/**
* vCalendar Instance for parsing
*
2015-06-22 18:20:15 +02:00
* @ var Horde_Icalendar
2010-10-23 13:43:52 +02:00
*/
var $vCalendar ;
2010-11-08 10:25:58 +01:00
2008-05-08 22:31:32 +02:00
var $filter_prop2cal = array (
'SUMMARY' => 'cal_title' ,
'UID' => 'cal_uid' ,
'DTSTART' => 'cal_start' ,
'DTEND' => 'cal_end' ,
// 'DURATION'
//'RRULE' => 'recur_type',
//'RDATE' => 'cal_start',
//'EXRULE'
//'EXDATE'
//'RECURRENCE-ID'
);
2010-02-11 21:50:35 +01:00
2009-12-27 05:21:33 +01:00
/**
* Does client understand exceptions to be included in VCALENDAR component of series master sharing its UID
2010-02-11 21:50:35 +01:00
*
2009-12-27 05:21:33 +01:00
* That also means no EXDATE for these exceptions !
2010-02-11 21:50:35 +01:00
*
2009-12-27 05:21:33 +01:00
* Setting it to false , should give the old behavior used in 1.6 ( hopefully ) no client needs that .
2010-02-11 21:50:35 +01:00
*
2009-12-27 05:21:33 +01:00
* @ var boolean
*/
var $client_shared_uid_exceptions = true ;
2011-11-08 22:09:06 +01:00
/**
* Enable or disable Schedule - Tag handling :
* - return Schedule - Tag header in PUT response
* - update only status and alarms of calendar owner , if If - Schedule - Tag - Match header in PUT
*
* Disabling Schedule - Tag for iCal , as current implementation seems to create too much trouble :- (
* - iCal on OS X always uses If - Schedule - Tag - Match , even if other stuff in event is changed ( eg . title )
* - iCal on iOS allways uses both If - Schedule - Tag - Match and If - Match ( ETag )
* - Lighting 1.0 is NOT using it
*
* @ var boolean
*/
2012-01-31 00:55:12 +01:00
var $use_schedule_tag = true ;
2011-11-08 22:09:06 +01:00
2009-12-27 05:21:33 +01:00
/**
2011-04-06 21:26:10 +02:00
* Are we using id , uid or caldav_name for the path / url
*
2016-04-02 12:44:17 +02:00
* Get 's set in constructor to ' caldav_name ' and self::$path_extension = ' ' !
2009-12-27 05:21:33 +01:00
*/
2011-04-06 21:26:10 +02:00
static $path_attr = 'id' ;
2008-05-08 22:31:32 +02:00
2023-02-15 19:50:28 +01:00
/**
* Contains IDs for multiget REPORT to be able to report missing ones
*
* @ var string []
*/
var $requested_multiget_ids ;
2008-05-17 15:00:34 +02:00
/**
* Constructor
*
* @ param string $app 'calendar' , 'addressbook' or 'infolog'
2016-04-02 12:44:17 +02:00
* @ param Api\CalDAV $caldav calling class
2008-05-17 15:00:34 +02:00
*/
2016-04-02 12:44:17 +02:00
function __construct ( $app , Api\CalDAV $caldav )
2008-05-08 22:31:32 +02:00
{
2016-04-02 12:44:17 +02:00
parent :: __construct ( $app , $caldav );
2008-05-08 22:31:32 +02:00
2009-06-08 18:21:14 +02:00
$this -> bo = new calendar_boupdate ();
2015-06-22 18:20:15 +02:00
$this -> vCalendar = new Horde_Icalendar ;
2011-04-06 21:26:10 +02:00
2023-07-21 17:41:37 +02:00
if ( Api\CalDAV :: isJSON ())
{
self :: $path_attr = 'id' ;
self :: $path_extension = '' ;
}
2011-04-06 21:26:10 +02:00
// since 1.9.003 we allow clients to specify the URL when creating a new event, as specified by CalDAV
2023-07-21 17:41:37 +02:00
elseif ( version_compare ( $GLOBALS [ 'egw_info' ][ 'apps' ][ 'calendar' ][ 'version' ], '1.9.003' , '>=' ))
2011-04-06 21:26:10 +02:00
{
self :: $path_attr = 'caldav_name' ;
2016-04-02 12:44:17 +02:00
self :: $path_extension = '' ;
2011-04-06 21:26:10 +02:00
}
2008-05-08 22:31:32 +02:00
}
2008-11-18 07:11:12 +01:00
/**
2018-09-20 15:56:19 +02:00
* Get grants of current user and app
*
2024-07-12 08:16:33 +02:00
* Overwritten to request rights for non - users ( $user is NOT numeric ) via calendars resource API .
2018-09-20 15:56:19 +02:00
*
2024-07-12 08:16:33 +02:00
* @ param ? string $user the user whose grants for the current user are requested , or null for all
2018-09-20 15:56:19 +02:00
* @ return array user - id => Api\Acl :: ADD | Api\Acl :: READ | Api\Acl :: EDIT | Api\Acl :: DELETE pairs
*/
2024-07-12 08:16:33 +02:00
public function get_grants ( string $user = null )
2018-09-20 15:56:19 +02:00
{
2024-07-12 08:16:33 +02:00
// grants from all regular users
$grants = $this -> bo -> grants ;
if ( ! ( int ) $user && ( $info = $this -> bo -> resource_info ( $user )))
{
$grants [ $user ] = $info [ 'rights' ] ? ? 0 ;
}
return $grants ;
2018-09-20 15:56:19 +02:00
}
/**
2008-11-18 07:11:12 +01:00
* Create the path for an event
*
* @ param array | int $event
* @ return string
*/
2010-05-09 22:23:53 +02:00
function get_path ( $event )
2008-11-18 07:11:12 +01:00
{
2011-04-06 21:26:10 +02:00
if ( is_numeric ( $event ) && self :: $path_attr == 'id' )
2008-11-18 07:11:12 +01:00
{
$name = $event ;
}
else
{
if ( ! is_array ( $event )) $event = $this -> bo -> read ( $event );
2011-04-06 21:26:10 +02:00
$name = $event [ self :: $path_attr ];
2008-11-18 07:11:12 +01:00
}
2016-04-02 12:44:17 +02:00
$name .= self :: $path_extension ;
//error_log(__METHOD__.'('.array2string($event).") path_attr='".self::$path_attr."', path_extension='".self::$path_extension."' returning ".array2string($name));
2011-04-06 21:26:10 +02:00
return $name ;
2008-11-18 07:11:12 +01:00
}
2013-03-14 18:13:59 +01:00
const PAST_LIMIT = 100 ;
const FUTURE_LIMIT = 365 ;
2008-05-08 22:31:32 +02:00
/**
* Handle propfind in the calendar folder
*
* @ param string $path
2012-06-27 22:08:56 +02:00
* @ param array & $options
2008-05-08 22:31:32 +02:00
* @ param array & $files
* @ param int $user account_id
2015-06-25 22:39:53 +02:00
* @ param string $id = ''
2008-05-08 22:31:32 +02:00
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
2012-06-27 22:08:56 +02:00
function propfind ( $path , & $options , & $files , $user , $id = '' )
2008-05-08 22:31:32 +02:00
{
2010-02-23 19:19:12 +01:00
if ( $this -> debug )
{
error_log ( __METHOD__ . " ( $path , " . array2string ( $options ) . " ,, $user , $id ) " );
}
2009-10-16 10:36:28 +02:00
2011-09-22 21:49:01 +02:00
if ( $options [ 'root' ][ 'name' ] == 'free-busy-query' )
{
return $this -> free_busy_report ( $path , $options , $user );
}
2008-05-08 22:31:32 +02:00
// ToDo: add parameter to only return id & etag
2010-03-15 10:55:16 +01:00
$filter = array (
2008-05-08 22:31:32 +02:00
'users' => $user ,
'enum_recuring' => false ,
'daywise' => false ,
'date_format' => 'server' ,
2011-04-05 17:32:20 +02:00
'no_total' => true , // we need no total number of rows (saves extra query)
2015-06-25 22:39:53 +02:00
'cfs' => array (), // return custom-fields, as we use them to store X- attributes
2008-05-08 22:31:32 +02:00
);
2013-03-14 18:13:59 +01:00
foreach ( array (
'start' => $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'groupdav' ][ 'calendar-past-limit' ],
'end' => $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'groupdav' ][ 'calendar-future-limit' ],
) as $name => $value )
{
if ( ! is_numeric ( $value ))
{
$value = $name == 'start' ? self :: PAST_LIMIT : self :: FUTURE_LIMIT ;
}
2013-05-16 14:26:59 +02:00
$filter [ $name ] = $this -> bo -> now + 24 * 3600 * ( $name == 'start' ? - 1 : 1 ) * abs ( $value );
2013-03-14 18:13:59 +01:00
}
2011-04-05 17:32:20 +02:00
if ( $this -> client_shared_uid_exceptions ) // do NOT return (non-virtual) exceptions
{
$filter [ 'query' ] = array ( 'cal_reference' => 0 );
}
2010-03-11 08:30:46 +01:00
2010-05-01 18:24:05 +02:00
if ( $path == '/calendar/' )
{
2015-10-09 19:47:10 +02:00
$filter [ 'filter' ] = 'owner' ;
2010-05-01 18:24:05 +02:00
}
2011-09-22 17:22:52 +02:00
// scheduling inbox, shows only not yet accepted or rejected events
elseif ( substr ( $path , - 7 ) == '/inbox/' )
{
$filter [ 'filter' ] = 'unknown' ;
2011-10-17 17:36:28 +02:00
$filter [ 'start' ] = $this -> bo -> now ; // only return future invitations
2011-09-22 17:22:52 +02:00
}
// ToDo: not sure what scheduling outbox is supposed to show, leave it empty for now
elseif ( substr ( $path , - 8 ) == '/outbox/' )
{
return true ;
}
2010-05-01 18:24:05 +02:00
else
{
$filter [ 'filter' ] = 'default' ; // not rejected
}
2010-03-11 08:30:46 +01:00
2008-05-08 22:31:32 +02:00
// process REPORT filters or multiget href's
2014-02-20 18:46:15 +01:00
$nresults = null ;
2012-09-26 16:30:47 +02:00
if (( $id || $options [ 'root' ][ 'name' ] != 'propfind' ) && ! $this -> _report_filters ( $options , $filter , $id , $nresults ))
2008-05-08 22:31:32 +02:00
{
2011-09-30 14:52:40 +02:00
// return empty collection, as iCal under iOS 5 had problems with returning "404 Not found" status
// when trying to request not supported components, eg. VTODO on a calendar collection
return true ;
2008-05-08 22:31:32 +02:00
}
2011-10-04 14:16:03 +02:00
if ( $id ) $path = dirname ( $path ) . '/' ; // caldav_name get's added anyway in the callback
2010-02-23 19:19:12 +01:00
if ( $this -> debug > 1 )
{
2011-04-05 17:32:20 +02:00
error_log ( __METHOD__ . " ( $path ,,, $user , $id ) filter= " . array2string ( $filter ));
2010-02-23 19:19:12 +01:00
}
2009-12-27 05:21:33 +01:00
2012-09-26 16:30:47 +02:00
// rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters
2024-05-13 19:18:44 +02:00
if ( $options [ 'root' ][ 'name' ] === 'sync-collection' )
2012-09-26 16:30:47 +02:00
{
// callback to query sync-token, after propfind_callbacks / iterator is run and
// stored max. modification-time in $this->sync_collection_token
$files [ 'sync-token' ] = array ( $this , 'get_sync_collection_token' );
$files [ 'sync-token-params' ] = array ( $path , $user );
2024-05-13 19:18:44 +02:00
$this -> sync_collection_token = $this -> more_results = null ;
2014-02-20 20:26:02 +01:00
$filter [ 'order' ] = 'cal_modified ASC' ; // return oldest modifications first
$filter [ 'sync-collection' ] = true ;
// no end-date / limit into the future, as unchanged entries would never be transferted later on
unset ( $filter [ 'end' ]);
2012-09-26 16:30:47 +02:00
}
2011-04-05 17:32:20 +02:00
2023-07-21 17:41:37 +02:00
// check if we have to return the full calendar data or just the etag's
if ( ! ( $filter [ 'calendar_data' ] = $options [ 'props' ] == 'all' &&
2023-08-09 14:57:40 +02:00
$options [ 'root' ][ 'ns' ] == Api\CalDAV :: CALDAV || isset ( $_GET [ 'download' ])) && is_array ( $options [ 'props' ]))
2023-07-21 17:41:37 +02:00
{
foreach ( $options [ 'props' ] as $prop )
{
if ( $prop [ 'name' ] == 'calendar-data' )
{
$filter [ 'calendar_data' ] = true ;
break ;
}
}
}
2024-05-13 19:18:44 +02:00
if ( isset ( $nresults ) && $options [ 'root' ][ 'name' ] === 'sync-collection' )
2012-09-26 16:30:47 +02:00
{
2014-02-20 18:46:15 +01:00
unset ( $filter [ 'no_total' ]); // we need the total!
2023-04-25 15:13:50 +02:00
$files [ 'files' ] = $this -> propfind_generator ( $path , $filter , $files [ 'files' ], ( int ) $nresults );
2012-09-26 16:30:47 +02:00
}
else
{
2023-02-15 19:50:28 +01:00
$files [ 'files' ] = $this -> propfind_generator ( $path , $filter , $files [ 'files' ]);
2012-09-26 16:30:47 +02:00
}
2015-11-13 16:23:36 +01:00
if ( isset ( $_GET [ 'download' ]))
2015-11-08 13:33:35 +01:00
{
$this -> output_vcalendar ( $files [ 'files' ]);
}
2010-03-15 10:55:16 +01:00
return true ;
}
2011-08-03 18:13:56 +02:00
2015-11-08 13:33:35 +01:00
/**
* Download whole calendar as big ics file
*
* @ param iterator | array $files
*/
function output_vcalendar ( $files )
{
// todo ETag logic with CTag to not download unchanged calendar again
2016-05-01 19:47:59 +02:00
Api\Header\Content :: type ( 'calendar.ics' , 'text/calendar' );
2015-11-08 13:33:35 +01:00
$n = 0 ;
foreach ( $files as $file )
{
if ( ! $n ++ ) continue ; // first entry is collection itself
$icalendar = $file [ 'props' ][ 'calendar-data' ][ 'val' ];
if (( $start = strpos ( $icalendar , 'BEGIN:VEVENT' )) !== false &&
( $end = strrpos ( $icalendar , 'END:VCALENDAR' )) !== false )
{
if ( $n === 2 )
{
2015-11-13 16:23:36 +01:00
// skip X-CALENDARSERVER-ACCESS:CONFIDENTIAL, as it is on VCALENDAR not VEVENT level
if (( $x_calendarserver_access = strpos ( $icalendar , 'X-CALENDARSERVER-ACCESS:' )) !== false )
{
echo substr ( $icalendar , 0 , $x_calendarserver_access );
}
2015-11-08 13:33:35 +01:00
// skip timezones, as we would need to collect them from all events
// ans most clients understand timezone by reference anyway
2015-11-13 16:23:36 +01:00
elseif (( $tz = strpos ( $icalendar , 'BEGIN:VTIMEZONE' )) !== false )
2015-11-08 13:33:35 +01:00
{
echo substr ( $icalendar , 0 , $tz );
}
else
{
echo substr ( $icalendar , 0 , $start );
}
}
echo substr ( $icalendar , $start , $end - $start );
}
}
if ( $icalendar && $end )
{
echo " END:VCALENDAR \n " ;
}
2016-05-01 19:47:59 +02:00
exit ();
2015-11-08 13:33:35 +01:00
}
2010-03-15 10:55:16 +01:00
/**
2023-05-10 14:15:30 +02:00
* Chunk - size for DB queries of propfind_generator
2023-02-15 19:50:28 +01:00
*/
const CHUNK_SIZE = 500 ;
/**
* Generator for propfind with ability to skip reporting not found ids
2010-03-15 10:55:16 +01:00
*
* @ param string $path
2023-02-15 19:50:28 +01:00
* @ param array & $filter
* @ param array $extra extra resources like the collection itself
* @ param int | null $nresults option limit of number of results to report
* @ return Generator < array with values for keys path and props >
2010-03-15 10:55:16 +01:00
*/
2023-02-15 19:50:28 +01:00
function propfind_generator ( $path , array & $filter , array $extra = [], $nresults = null )
2010-03-15 10:55:16 +01:00
{
2010-08-05 19:11:13 +02:00
if ( $this -> debug ) $starttime = microtime ( true );
2023-07-21 17:41:37 +02:00
$calendar_data = $filter [ 'calendar_data' ];
unset ( $filter [ 'calendar_data' ]);
2011-04-05 17:32:20 +02:00
2023-02-15 19:50:28 +01:00
// yield extra resources like the root itself
$yielded = 0 ;
foreach ( $extra as $resource )
2023-02-15 08:45:28 +01:00
{
2023-02-15 19:50:28 +01:00
if ( ++ $yielded && isset ( $nresults ) && $yielded > $nresults )
{
2024-05-13 19:18:44 +02:00
$this -> sync_collection_token = Api\DateTime :: user2server ( $resource [ 'modified' ], 'ts' ) - 1 ;
$this -> more_results = true ;
2023-02-15 19:50:28 +01:00
return ;
}
yield $resource ;
2023-02-15 08:45:28 +01:00
}
2014-02-20 20:26:02 +01:00
$sync_collection = $filter [ 'sync-collection' ];
2013-01-25 18:52:28 +01:00
2023-05-10 14:15:30 +02:00
$events = null ;
2023-07-21 17:41:37 +02:00
$is_jscalendar = Api\CalDAV :: isJSON ();
2023-05-10 14:15:30 +02:00
for ( $chunk = 0 ; ( ! $chunk || count ( $events ) === self :: CHUNK_SIZE ) && // stop after we have not got a full chunk
2023-07-24 17:08:05 +02:00
( $events =& $this -> bo -> search ([
2023-05-10 14:15:30 +02:00
'offset' => $chunk * self :: CHUNK_SIZE ,
'num_rows' => self :: CHUNK_SIZE ,
2023-07-28 14:23:18 +02:00
'date_format' => $is_jscalendar ? 'object' : 'server' ,
2023-07-24 17:08:05 +02:00
] + $filter )); ++ $chunk )
2008-05-08 22:31:32 +02:00
{
2010-03-16 21:44:55 +01:00
foreach ( $events as $event )
2008-05-08 22:31:32 +02:00
{
2023-05-10 14:15:30 +02:00
$no_active_participants = ! $this -> hasActiveParticipants ( $event , $filter [ 'users' ], $exceptions );
2023-04-25 15:13:50 +02:00
2024-05-13 19:18:44 +02:00
if ( ++ $yielded && isset ( $nresults ) && $yielded > $nresults )
{
$this -> sync_collection_token = Api\DateTime :: user2server ( $event [ 'modified' ], 'ts' ) - 1 ;
$this -> more_results = true ;
return ;
}
2012-09-26 16:30:47 +02:00
// sync-collection report: deleted entries need to be reported without properties, same for rejected or deleted invitations
2023-04-25 15:13:50 +02:00
if ( $sync_collection && ( $event [ 'deleted' ] && ! $event [ 'cal_reference' ] ||
//in_array($event['participants'][$filter['users']][0] ?? '', array('R','X')) ||
$no_active_participants ))
2012-09-26 16:30:47 +02:00
{
2023-04-25 15:13:50 +02:00
// remove event from requested multiget ids, to be able to report not found urls
if ( ! empty ( $this -> requested_multiget_ids ) && ( $k = array_search ( $event [ self :: $path_attr ], $this -> requested_multiget_ids )) !== false )
{
unset ( $this -> requested_multiget_ids [ $k ]);
}
2023-02-15 19:50:28 +01:00
yield [ 'path' => $path . urldecode ( $this -> get_path ( $event ))];
2012-09-26 16:30:47 +02:00
continue ;
}
2023-04-25 15:13:50 +02:00
// for a regular propfind/multiget-report we must NOT return deleted or rejected events
if ( ! $sync_collection && $no_active_participants )
{
continue ;
}
// remove event from requested multiget ids, to be able to report not found urls
if ( ! empty ( $this -> requested_multiget_ids ) && ( $k = array_search ( $event [ self :: $path_attr ], $this -> requested_multiget_ids )) !== false )
{
unset ( $this -> requested_multiget_ids [ $k ]);
}
2015-06-25 22:39:53 +02:00
$schedule_tag = null ;
2011-10-20 22:10:04 +02:00
$etag = $this -> get_etag ( $event , $schedule_tag );
2012-10-08 13:14:07 +02:00
2008-06-04 13:07:45 +02:00
//header('X-EGROUPWARE-EVENT-'.$event['id'].': '.$event['title'].': '.date('Y-m-d H:i:s',$event['start']).' - '.date('Y-m-d H:i:s',$event['end']));
2008-05-08 22:31:32 +02:00
$props = array (
2023-07-21 17:41:37 +02:00
'getcontenttype' => $is_jscalendar ? Api\CalDAV\JsCalendar :: MIME_TYPE_JSEVENT :
( $this -> agent != 'kde' ? 'text/calendar; charset=utf-8; component=VEVENT' : 'text/calendar' ),
2011-10-20 22:10:04 +02:00
'getetag' => '"' . $etag . '"' ,
2024-05-13 19:18:44 +02:00
'displayname' => $event [ 'title' ],
'getlastmodified' => Api\DateTime :: user2server ( $event [ 'modified' ], 'ts' ),
2012-10-08 13:14:07 +02:00
// user and timestamp of creation or last modification of event, used in calendarserver only for shared calendars
2016-04-02 12:44:17 +02:00
'created-by' => Api\CalDAV :: mkprop ( Api\CalDAV :: CALENDARSERVER , 'created-by' ,
2012-10-08 13:14:07 +02:00
$this -> _created_updated_by_prop ( $event [ 'creator' ], $event [ 'created' ])),
2016-04-02 12:44:17 +02:00
'updated-by' => Api\CalDAV :: mkprop ( Api\CalDAV :: CALENDARSERVER , 'updated-by' ,
2012-10-08 13:14:07 +02:00
$this -> _created_updated_by_prop ( $event [ 'modifier' ], $event [ 'modified' ])),
2008-05-08 22:31:32 +02:00
);
2011-11-08 22:09:06 +01:00
if ( $this -> use_schedule_tag )
{
2016-04-02 12:44:17 +02:00
$props [ 'schedule-tag' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'schedule-tag' , '"' . $schedule_tag . '"' );
2011-11-08 22:09:06 +01:00
}
2009-04-02 14:35:26 +02:00
//error_log(__FILE__ . __METHOD__ . "Calendar Data : $calendar_data");
2008-05-08 22:31:32 +02:00
if ( $calendar_data )
{
2023-07-24 17:08:05 +02:00
$content = $this -> iCal ( $event , $filter [ 'users' ],
2012-01-24 06:04:35 +01:00
strpos ( $path , '/inbox/' ) !== false ? 'REQUEST' : null ,
! isset ( $calendar_data [ 'children' ][ 'expand' ]) ? false :
2023-07-24 17:08:05 +02:00
( $calendar_data [ 'children' ][ 'expand' ][ 'attrs' ] ? : true ), $exceptions , $is_jscalendar ? false : null );
2023-07-21 17:41:37 +02:00
$props [ 'getcontentlength' ] = bytes ( $is_jscalendar ? json_encode ( $content ) : $content );
2016-04-02 12:44:17 +02:00
$props [ 'calendar-data' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'calendar-data' , $content );
2008-05-20 11:07:03 +02:00
}
2011-10-20 22:10:04 +02:00
/* Calendarserver reports new events with schedule - changes : action : create , which iCal request
* adding it , unfortunately does not lead to showing the new event in the users inbox
2016-04-02 12:44:17 +02:00
if ( strpos ( $path , '/inbox/' ) !== false && $this -> caldav -> prop_requested ( 'schedule-changes' ))
2011-10-20 22:10:04 +02:00
{
2016-04-02 12:44:17 +02:00
$props [ 'schedule-changes' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALENDARSERVER , 'schedule-changes' , array (
Api\CalDAV :: mkprop ( Api\CalDAV :: CALENDARSERVER , 'dtstamp' , gmdate ( 'Ymd\THis' , $event [ 'created' ]) . 'Z' ),
Api\CalDAV :: mkprop ( Api\CalDAV :: CALENDARSERVER , 'action' , array (
Api\CalDAV :: mkprop ( Api\CalDAV :: CALENDARSERVER , 'create' , '' ),
2011-10-20 22:10:04 +02:00
)),
));
} */
2023-02-15 19:50:28 +01:00
yield $this -> add_resource ( $path , $event , $props );
2008-05-08 22:31:32 +02:00
}
2013-09-25 12:27:41 +02:00
}
// report not found multiget urls
2023-02-15 19:50:28 +01:00
if ( ! empty ( $this -> requested_multiget_ids ))
2013-09-25 12:27:41 +02:00
{
2023-02-15 19:50:28 +01:00
foreach ( $this -> requested_multiget_ids as $id )
2012-09-26 16:30:47 +02:00
{
2023-02-15 19:50:28 +01:00
if ( ++ $yielded && isset ( $nresults ) && $yielded > $nresults )
{
return ;
}
yield [ 'path' => $path . $id . self :: $path_extension ];
2012-09-26 16:30:47 +02:00
}
2008-05-08 22:31:32 +02:00
}
2013-09-25 12:27:41 +02:00
// sync-collection report --> return modified of last contact as sync-token
2014-02-20 18:46:15 +01:00
if ( $sync_collection )
2013-09-25 12:27:41 +02:00
{
$this -> sync_collection_token = $event [ 'modified' ];
}
2010-02-23 19:19:12 +01:00
if ( $this -> debug )
{
error_log ( __METHOD__ . " ( $path ) took " . ( microtime ( true ) - $starttime ) .
2023-02-15 19:50:28 +01:00
" to return $yielded resources " );
2010-02-23 19:19:12 +01:00
}
2008-05-08 22:31:32 +02:00
}
2023-04-25 15:13:50 +02:00
/**
* Check an event has active ( not rejected or deleted ) participants against an ( array of ) user - or group - id ( s )
*
* For recurring events / series - master we have to check all exceptions too !
2023-05-10 14:15:30 +02:00
* For a group - id we also check against all members and for a user - id we also check the memberships !
2023-04-25 15:13:50 +02:00
*
* @ param array $event
* @ param int | int [] $users user ( s ) to check
2023-05-10 14:15:30 +02:00
* @ param array | null & $exceptions on return exceptions to the series as returned by self :: get_series () ( $expand = false ! )
2023-04-25 15:13:50 +02:00
* @ return bool true : user ( s ) is an active participant , or false if not ( incl . all exceptions for recurring events )
*/
2023-05-10 14:15:30 +02:00
protected function hasActiveParticipants ( array $event , $users , array & $exceptions = null ) : bool
2023-04-25 15:13:50 +02:00
{
2023-05-10 14:15:30 +02:00
$exceptions = null ;
2023-04-25 15:13:50 +02:00
if ( ! is_array ( $users ))
{
2023-05-10 14:15:30 +02:00
$add = $users < 0 ? 'members' : 'memberships' ;
$users = array_merge ([ $users ], Api\Accounts :: getInstance () -> $add ( $users , true ) ? : []);
2023-04-25 15:13:50 +02:00
}
// return true event(-master) has active participants
foreach ( array_intersect_key ( $event [ 'participants' ], array_flip ( $users )) as $status )
{
if ( ! in_array ( $status [ 0 ], [ 'R' , 'X' ]))
{
//error_log(__METHOD__."(cal_id=$event[id]: $event[title], cal_reference=$event[cal_reference], participants=".json_encode($event['participants']).", ".json_encode($users)." returning true");
return true ;
}
}
// return false for regular / non-recurring events
if ( empty ( $event [ 'recur_type' ]))
{
return false ;
}
// check exceptions for recurring event
2023-05-10 14:15:30 +02:00
$exceptions =& self :: get_series ( $event [ 'uid' ], $this -> bo , false , $users [ 0 ], $event );
foreach ( $exceptions as $exception )
2023-04-25 15:13:50 +02:00
{
if ( empty ( $exception [ 'reference' ])) continue ; // master / identical to $event and already checked
if ( $this -> hasActiveParticipants ( $exception , $users ))
{
return true ;
}
}
//error_log(__METHOD__."(cal_id=$event[id]: $event[title], cal_reference=$event[cal_reference], participants=".json_encode($event['participants']).", ".json_encode($users)." returning false");
// if we get here, given user(s) are NOT an active participant of any exception or has been deleted or rejected them all
return false ;
}
2012-10-08 13:14:07 +02:00
/**
* Return Calendarserver : ( created | updated ) - by sub - properties for a given user and time
*
* < created - by xmlns = 'http://calendarserver.org/ns/' >
* < first - name > Ralf </ first - name >
* < last - name > Becker </ last - name >
* < dtstamp > 20121002 T092006Z </ dtstamp >
* < href xmlns = 'DAV:' > mailto : farktronix @ me . com </ href >
* </ created - by >
*
* @ param int $user
* @ param int $time
* @ return array with subprops
*/
private function _created_updated_by_prop ( $user , $time )
{
$props = array ();
foreach ( array (
'first-name' => 'account_firstname' ,
'last-name' => 'account_lastname' ,
'href' => 'account_email' ,
) as $prop => $name )
{
if ( $user && ( $val = $this -> accounts -> id2name ( $user , $name )))
{
2016-04-02 12:44:17 +02:00
$ns = Api\CalDAV :: CALENDARSERVER ;
2012-10-08 13:14:07 +02:00
if ( $prop == 'href' )
{
$ns = '' ;
$val = 'mailto:' . $val ;
}
2016-04-02 12:44:17 +02:00
$props [ $prop ] = $ns ? Api\CalDAV :: mkprop ( $ns , $prop , $val ) : Api\CalDAV :: mkprop ( $prop , $val );
2012-10-08 13:14:07 +02:00
}
}
if ( $time )
{
2023-07-24 17:08:05 +02:00
$dtstamp = Api\DateTime :: to ( $time , 'object' );
$dtstamp -> setTimezone ( new DateTimeZone ( 'utc' ));
$props [ 'dtstamp' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALENDARSERVER , 'dtstamp' , $dtstamp -> format ( 'Ymd\\This\\Z' ));
2012-10-08 13:14:07 +02:00
}
//error_log(__METHOD__."($user, $time) returning ".array2string($props));
2023-07-24 17:08:05 +02:00
return $props ? : '' ;
2012-10-08 13:14:07 +02:00
}
2008-05-08 22:31:32 +02:00
/**
* Process the filters from the CalDAV REPORT request
*
* @ param array $options
* @ param array & $cal_filters
* @ param string $id
2023-04-25 15:13:50 +02:00
* @ param int & $nresults on return limit for number of results or unchanged / null
2012-09-26 16:30:47 +02:00
* @ return boolean true if filter could be processed
2008-05-08 22:31:32 +02:00
*/
2012-09-26 16:30:47 +02:00
function _report_filters ( $options , & $cal_filters , $id , & $nresults )
2008-05-08 22:31:32 +02:00
{
if ( $options [ 'filters' ])
{
// unset default start & end
$cal_start = $cal_filters [ 'start' ]; unset ( $cal_filters [ 'start' ]);
$cal_end = $cal_filters [ 'end' ]; unset ( $cal_filters [ 'end' ]);
$num_filters = count ( $cal_filters );
foreach ( $options [ 'filters' ] as $filter )
{
switch ( $filter [ 'name' ])
{
case 'comp-filter' :
2009-12-27 05:21:33 +01:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " ( $options[path] ,...) comp-filter=' { $filter [ 'attrs' ][ 'name' ] } ' " );
2009-07-14 21:51:03 +02:00
2008-05-08 22:31:32 +02:00
switch ( $filter [ 'attrs' ][ 'name' ])
{
case 'VTODO' :
return false ; // return nothing for now, todo: check if we can pass it on to the infolog handler
// todos are handled by the infolog handler
2009-12-27 05:21:33 +01:00
//$infolog_handler = new groupdav_infolog();
2010-03-07 00:06:43 +01:00
//return $infolog_handler->propfind($options['path'],$options,$options['files'],$user,$method);
2008-05-08 22:31:32 +02:00
case 'VCALENDAR' :
2009-04-02 14:35:26 +02:00
case 'VEVENT' :
2008-05-08 22:31:32 +02:00
break ; // that's our default anyway
}
break ;
case 'prop-filter' :
2009-12-27 05:21:33 +01:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " ( $options[path] ,...) prop-filter=' { $filter [ 'attrs' ][ 'name' ] } ' " );
2008-05-08 22:31:32 +02:00
$prop_filter = $filter [ 'attrs' ][ 'name' ];
break ;
case 'text-match' :
2009-12-27 05:21:33 +01:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " ( $options[path] ,...) text-match: $prop_filter =' { $filter [ 'data' ] } ' " );
2008-05-08 22:31:32 +02:00
if ( ! isset ( $this -> filter_prop2cal [ strtoupper ( $prop_filter )]))
{
2009-12-27 05:21:33 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " ( $options[path] , " . array2string ( $options ) . " ,...) unknown property ' $prop_filter ' --> ignored " );
2008-05-08 22:31:32 +02:00
}
else
{
$cal_filters [ 'query' ][ $this -> filter_prop2cal [ strtoupper ( $prop_filter )]] = $filter [ 'data' ];
}
unset ( $prop_filter );
break ;
case 'param-filter' :
2009-12-27 05:21:33 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " ( $options[path] ,...) param-filter=' { $filter [ 'attrs' ][ 'name' ] } ' not (yet) implemented! " );
2008-05-08 22:31:32 +02:00
break ;
case 'time-range' :
2009-12-27 05:21:33 +01:00
if ( $this -> debug > 1 ) error_log ( __FILE__ . __METHOD__ . " ( $options[path] ,...) time-range= { $filter [ 'attrs' ][ 'start' ] } - { $filter [ 'attrs' ][ 'end' ] } " );
2010-10-23 13:43:52 +02:00
if ( ! empty ( $filter [ 'attrs' ][ 'start' ]))
{
$cal_filters [ 'start' ] = $this -> vCalendar -> _parseDateTime ( $filter [ 'attrs' ][ 'start' ]);
}
if ( ! empty ( $filter [ 'attrs' ][ 'end' ]))
{
$cal_filters [ 'end' ] = $this -> vCalendar -> _parseDateTime ( $filter [ 'attrs' ][ 'end' ]);
}
2008-05-08 22:31:32 +02:00
break ;
default :
2009-12-27 05:21:33 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " ( $options[path] , " . array2string ( $options ) . " ,...) unknown filter --> ignored " );
2008-05-08 22:31:32 +02:00
break ;
}
}
2008-06-04 13:07:45 +02:00
if ( count ( $cal_filters ) == $num_filters ) // no filters set --> restore default start and end time
2008-05-08 22:31:32 +02:00
{
$cal_filters [ 'start' ] = $cal_start ;
$cal_filters [ 'end' ] = $cal_end ;
}
}
2011-09-22 17:22:52 +02:00
2012-09-26 16:30:47 +02:00
// parse limit from $options['other']
/* Example limit
< B : limit >
< B : nresults > 10 </ B : nresults >
</ B : limit >
*/
foreach (( array ) $options [ 'other' ] as $option )
{
switch ( $option [ 'name' ])
{
case 'nresults' :
$nresults = ( int ) $option [ 'data' ];
//error_log(__METHOD__."(...) options[other]=".array2string($options['other'])." --> nresults=$nresults");
break ;
case 'limit' :
break ;
case 'href' :
break ; // from addressbook-multiget, handled below
// rfc 6578 sync-report
case 'sync-token' :
if ( ! empty ( $option [ 'data' ]))
{
$parts = explode ( '/' , $option [ 'data' ]);
$sync_token = array_pop ( $parts );
$cal_filters [ 'query' ][] = 'cal_modified>' . ( int ) $sync_token ;
$cal_filters [ 'filter' ] = 'everything' ; // to return deleted entries too
// no standard time-range!
unset ( $cal_filters [ 'start' ]);
}
break ;
case 'sync-level' :
if ( $option [ 'data' ] != '1' )
{
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( __METHOD__ . " (...) only sync-level { $option [ 'data' ] } requested, but only 1 supported! options[other]= " . array2string ( $options [ 'other' ]));
2012-09-26 16:30:47 +02:00
}
break ;
default :
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( __METHOD__ . " (...) unknown xml tag ' { $option [ 'name' ] } ': options[other]= " . array2string ( $options [ 'other' ]));
2012-09-26 16:30:47 +02:00
break ;
}
}
2008-05-08 22:31:32 +02:00
// multiget or propfind on a given id
2009-04-02 14:35:26 +02:00
//error_log(__FILE__ . __METHOD__ . "multiget of propfind:");
2023-02-15 19:50:28 +01:00
$this -> requested_multiget_ids = null ;
2008-05-08 22:31:32 +02:00
if ( $options [ 'root' ][ 'name' ] == 'calendar-multiget' || $id )
{
// no standard time-range!
unset ( $cal_filters [ 'start' ]);
unset ( $cal_filters [ 'end' ]);
if ( $id )
{
2016-04-02 12:44:17 +02:00
$cal_filters [ 'query' ][ self :: $path_attr ] = self :: $path_extension ?
basename ( $id , self :: $path_extension ) : $id ;
2008-05-08 22:31:32 +02:00
}
else // fetch all given url's
{
2023-02-15 19:50:28 +01:00
$this -> requested_multiget_ids = [];
2008-05-08 22:31:32 +02:00
foreach ( $options [ 'other' ] as $option )
{
if ( $option [ 'name' ] == 'href' )
{
$parts = explode ( '/' , $option [ 'data' ]);
2014-02-20 16:11:27 +01:00
if (( $id = urldecode ( array_pop ( $parts ))))
2010-11-08 10:25:58 +01:00
{
2023-02-15 19:50:28 +01:00
$this -> requested_multiget_ids [] = self :: $path_extension ?
2016-04-02 12:44:17 +02:00
basename ( $id , self :: $path_extension ) : $id ;
2010-11-08 10:25:58 +01:00
}
2008-05-08 22:31:32 +02:00
}
}
2023-02-15 19:50:28 +01:00
$cal_filters [ 'query' ][ self :: $path_attr ] = $this -> requested_multiget_ids ;
2008-05-08 22:31:32 +02:00
}
2009-07-14 21:51:03 +02:00
2023-04-25 15:13:50 +02:00
if ( $this -> debug > 1 )
{
error_log ( __FILE__ . __METHOD__ . " ( $options[path] ,..., $id ) calendar-multiget: ids= " .
implode ( ',' , $this -> requested_multiget_ids ) . ', cal_filters=' . json_encode ( $cal_filters ));
}
2008-05-08 22:31:32 +02:00
}
return true ;
}
/**
* Handle get request for an event
*
* @ param array & $options
* @ param int $id
2015-06-25 22:39:53 +02:00
* @ param int $user = null account_id
2008-05-08 22:31:32 +02:00
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
2011-03-05 11:21:32 +01:00
function get ( & $options , $id , $user = null )
2008-05-08 22:31:32 +02:00
{
if ( ! is_array ( $event = $this -> _common_get_put_delete ( 'GET' , $options , $id )))
{
return $event ;
}
2011-10-17 17:36:28 +02:00
2023-07-21 17:41:37 +02:00
// jsEvent or iCal
2024-02-06 15:39:12 +01:00
if (( $type = Api\CalDAV :: isJSON ( $_SERVER [ 'HTTP_ACCEPT' ])) || ( $type = Api\CalDAV :: isJSON ()))
2023-07-21 17:41:37 +02:00
{
2023-07-24 17:08:05 +02:00
$options [ 'data' ] = $this -> iCal ( $event , $user , strpos ( $options [ 'path' ], '/inbox/' ) !== false ? 'REQUEST' : null , false , null , $type );
2023-07-21 17:41:37 +02:00
$options [ 'mimetype' ] = Api\CalDAV\JsCalendar :: MIME_TYPE_JSEVENT . ';charset=utf-8' ;
}
else
{
2023-07-24 17:08:05 +02:00
$options [ 'data' ] = $this -> iCal ( $event , $user , strpos ( $options [ 'path' ], '/inbox/' ) !== false ? 'REQUEST' : null , false , null );
2023-07-21 17:41:37 +02:00
$options [ 'mimetype' ] = 'text/calendar; charset=utf-8' ;
}
2008-05-08 22:31:32 +02:00
header ( 'Content-Encoding: identity' );
2015-06-25 22:39:53 +02:00
$schedule_tag = null ;
2011-10-20 22:10:04 +02:00
header ( 'ETag: "' . $this -> get_etag ( $event , $schedule_tag ) . '"' );
2011-11-08 22:09:06 +01:00
if ( $this -> use_schedule_tag )
{
header ( 'Schedule-Tag: "' . $schedule_tag . '"' );
}
2008-05-08 22:31:32 +02:00
return true ;
}
2010-02-11 21:50:35 +01:00
2009-12-27 05:21:33 +01:00
/**
* Generate an iCal for the given event
2010-02-11 21:50:35 +01:00
*
2023-07-24 17:08:05 +02:00
* Taking into account virtual and real exceptions for recurring events
2010-02-11 21:50:35 +01:00
*
2009-12-27 05:21:33 +01:00
* @ param array $event
2015-06-25 22:39:53 +02:00
* @ param int $user = null account_id of calendar to display
2023-07-24 17:08:05 +02:00
* @ param string $method = null e . g . 'PUBLISH' for inbox , nothing anywhere else
2015-06-25 22:39:53 +02:00
* @ param boolean | array $expand = false true or array with values for 'start' , 'end' to expand recurrences
2023-04-25 15:13:50 +02:00
* @ param array | null $events as returned by get_series () for ! $expand ( to not read them again )
2023-07-24 17:08:05 +02:00
* @ param bool | " pretty " | null $json null : iCal , false : return array with JSCalendar data , true + " pretty " : return json - serialized JSCalendar
* @ return string | array array if $json === false
2009-12-27 05:21:33 +01:00
*/
2023-07-24 17:08:05 +02:00
private function iCal ( array $event , $user = null , $method = null , $expand = false , array $events = null , $json = null )
2009-12-27 05:21:33 +01:00
{
static $handler = null ;
if ( is_null ( $handler )) $handler = $this -> _get_handler ();
2010-02-11 21:50:35 +01:00
2011-03-05 11:21:32 +01:00
if ( ! $user ) $user = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
// only return alarms in own calendar, not other users calendars
if ( $user != $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ])
{
//error_log(__METHOD__.'('.array2string($event).", $user) clearing alarms");
$event [ 'alarm' ] = array ();
}
2020-06-09 18:26:59 +02:00
else if ( $event [ '##videoconference' ])
{
// Add video conference link to description for user's own calendar
$avatar = new Api\Contacts\Photo ( " account: $user " ,
// disable sharing links currently, as sharing links from a different EGroupware user destroy the session
true );
2021-01-06 11:31:49 +01:00
// wrap in try and catch in case videoconference throws exceptions. e.g. BBB server exceptions
try {
$link = EGroupware\Status\Videoconference\Call :: genMeetingUrl ( $event [ '##videoconference' ], [
2020-06-09 18:26:59 +02:00
'name' => Api\Accounts :: username ( $user ),
'email' => Api\Accounts :: id2name ( $user , 'account_email' ),
'avatar' => ( string ) $avatar ,
2020-11-27 12:20:44 +01:00
'account_id' => $user ,
2021-05-21 13:07:30 +02:00
'cal_id' => $event [ 'id' ],
'notify_only' => true
2021-01-11 14:38:52 +01:00
], [ 'participants' => array_filter ( $event [ 'participants' ], function ( $key ){ return is_numeric ( $key );}, ARRAY_FILTER_USE_KEY )], $event [ 'start_date' ], $event [ 'end_date' ]);
2021-01-06 11:31:49 +01:00
} catch ( Exception $e )
{
//error_log(__METHOD__.'()'.$e->getMessage());
// do nothing
}
2020-06-09 18:26:59 +02:00
$event [ 'description' ] = lang ( 'Videoconference' ) . " : \n $link\n\n " . $event [ 'description' ];
}
2011-03-05 11:21:32 +01:00
2023-04-25 15:13:50 +02:00
// for recurring events we have to add the exceptions
2009-12-27 05:21:33 +01:00
if ( $this -> client_shared_uid_exceptions && $event [ 'recur_type' ] && ! empty ( $event [ 'uid' ]))
{
2024-07-04 15:46:49 +02:00
// some clients (Apple, eMclient) do NOT understand RDATE, therefore we expand the recurrences
if ( $event [ 'recur_type' ] == calendar_rrule :: RDATE && in_array ( Api\CalDAV\Handler :: get_agent (), [ 'dataaccess' , 'iphone' , 'calendaragent' , 'emclient' ]))
{
$expand = $readd_master = true ;
}
2012-01-24 06:04:35 +01:00
if ( is_array ( $expand ))
{
if ( isset ( $expand [ 'start' ])) $expand [ 'start' ] = $this -> vCalendar -> _parseDateTime ( $expand [ 'start' ]);
if ( isset ( $expand [ 'end' ])) $expand [ 'end' ] = $this -> vCalendar -> _parseDateTime ( $expand [ 'end' ]);
}
2020-01-10 11:13:18 +01:00
// pass in original event as master, as it has correct start-date even if first recurrence is an exception
2023-04-25 15:13:50 +02:00
if ( $expand || ! isset ( $events ))
{
2023-07-24 17:08:05 +02:00
$events =& self :: get_series ( $event [ 'uid' ], $this -> bo , $expand , $user , $event , isset ( $json ) ? 'object' : 'server' );
2024-07-04 15:46:49 +02:00
if ( ! empty ( $readd_master ))
{
array_unshift ( $events , $event );
}
2023-04-25 15:13:50 +02:00
}
2017-09-21 17:52:25 +02:00
// as alarm is now only on next recurrence, set alarm from original event on master
if ( $event [ 'alarm' ]) $events [ 0 ][ 'alarm' ] = $event [ 'alarm' ];
2009-12-27 05:21:33 +01:00
}
2023-04-25 15:13:50 +02:00
else
2009-12-27 05:21:33 +01:00
{
2023-04-25 15:13:50 +02:00
$events = array ( $event );
if ( ! $this -> client_shared_uid_exceptions && $event [ 'reference' ])
{
$events [ 0 ][ 'uid' ] .= '-' . $event [ 'id' ]; // force a different uid
}
2009-12-27 05:21:33 +01:00
}
2023-07-24 17:08:05 +02:00
if ( isset ( $json ))
{
2024-04-12 20:14:39 +02:00
// master aka $events[0] might not be set, for a series where current user only participates in one or *more* exceptions
return Api\CalDAV\JsCalendar :: JsEvent ( array_shift ( $events ), $json , $events );
2023-07-24 17:08:05 +02:00
}
2011-10-03 14:53:28 +02:00
return $handler -> exportVCal ( $events , '2.0' , $method );
2009-12-27 05:21:33 +01:00
}
2010-02-11 21:50:35 +01:00
2009-12-27 05:21:33 +01:00
/**
* Get array with events of a series identified by its UID ( master and all exceptions )
2010-02-11 21:50:35 +01:00
*
2009-12-27 05:21:33 +01:00
* Maybe that should be part of calendar_bo
2010-02-11 21:50:35 +01:00
*
2009-12-27 05:21:33 +01:00
* @ param string $uid UID
2015-06-25 22:39:53 +02:00
* @ param calendar_bo $bo = null calendar_bo object to reuse for search call
* @ param boolean | array $expand = false true or array with values for 'start' , 'end' to expand recurrences
* @ param int $user = null account_id of calendar to display , to remove master , if current user does not participate in
2020-01-10 11:13:18 +01:00
* @ param array $master = null use provided event as master to fix wrong start - date if first recurrence is an exception
2009-12-27 05:21:33 +01:00
* @ return array
*/
2023-07-24 17:08:05 +02:00
private static function & get_series ( $uid , calendar_bo $bo = null , $expand = false , $user = null , $master = null , $date_format = 'server' )
2009-12-27 05:21:33 +01:00
{
2022-09-13 19:35:00 +02:00
if ( is_null ( $bo )) $bo = new calendar_boupdate ();
2010-02-17 14:29:28 +01:00
2012-01-24 06:04:35 +01:00
$params = array (
2009-12-27 05:21:33 +01:00
'query' => array ( 'cal_uid' => $uid ),
2010-05-19 17:25:07 +02:00
'filter' => 'owner' , // return all possible entries
2009-12-27 05:21:33 +01:00
'daywise' => false ,
2023-07-24 17:08:05 +02:00
'date_format' => $date_format ,
2015-06-25 22:39:53 +02:00
'cfs' => array (), // read cfs as we use them to store X- attributes
2012-01-24 06:04:35 +01:00
);
if ( is_array ( $expand )) $params += $expand ;
2015-10-09 19:37:01 +02:00
if ( ! ( $events =& $bo -> search ( $params )))
{
2023-04-25 15:13:50 +02:00
$events = [];
return $events ;
2015-10-09 19:37:01 +02:00
}
2012-01-24 06:04:35 +01:00
2023-07-24 17:08:05 +02:00
// find master, which is not always first event, e.g. when first event is an exception
2015-10-09 19:37:01 +02:00
$exceptions = array ();
2009-12-27 05:21:33 +01:00
foreach ( $events as $k => & $recurrence )
{
2015-01-27 19:12:10 +01:00
if ( $recurrence [ 'recur_type' ])
2014-11-10 21:04:37 +01:00
{
2020-01-10 11:13:18 +01:00
if ( ! isset ( $master )) $master = $recurrence ;
2014-11-10 21:04:37 +01:00
$exceptions =& $master [ 'recur_exception' ];
unset ( $events [ $k ]);
2015-01-27 19:12:10 +01:00
break ;
2014-11-10 21:04:37 +01:00
}
2015-01-27 19:12:10 +01:00
}
2015-02-04 11:37:52 +01:00
// if recurring event starts in future behind horizont, nothing will be returned by bo::search()
2023-07-24 17:08:05 +02:00
if ( ! isset ( $master )) $master = $bo -> read ( $uid , null , false , $date_format );
2015-10-09 19:37:01 +02:00
2015-01-27 19:12:10 +01:00
foreach ( $events as $k => & $recurrence )
{
2012-01-24 06:04:35 +01:00
//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($uid)[$k]:" . array2string($recurrence));
2020-01-07 17:31:25 +01:00
if ( $master && $recurrence [ 'reference' ] && $recurrence [ 'reference' ] != $master [ 'id' ])
2017-09-21 17:52:25 +02:00
{
unset ( $events [ $k ]);
2023-07-24 17:08:05 +02:00
continue ; // same uid, but references a different event or its own master
2017-09-21 17:52:25 +02:00
}
2015-10-09 19:37:01 +02:00
if ( ! $master || $recurrence [ 'id' ] != $master [ 'id' ]) // real exception
2009-12-27 05:21:33 +01:00
{
2014-11-10 21:04:37 +01:00
// user is NOT participating in this exception
2015-01-27 19:12:10 +01:00
if ( $user && ! self :: isParticipant ( $recurrence , $user ))
2014-11-10 21:04:37 +01:00
{
// if he is NOT in master, delete this exception
2015-10-09 19:37:01 +02:00
if ( ! $master || ! self :: isParticipant ( $master , $user ))
2014-11-10 21:04:37 +01:00
{
unset ( $events [ $k ]);
continue ;
}
// otherwise mark him in this exception as rejected
$recurrence [ 'participants' ][ $user ] = 'R' ;
}
2009-12-27 05:21:33 +01:00
//error_log('real exception: '.array2string($recurrence));
2023-07-24 17:08:05 +02:00
// remove from masters recur_exception, as exception is included
2010-02-11 21:50:35 +01:00
// at least Lightning "understands" EXDATE as exception from what's included
2009-12-27 05:21:33 +01:00
// in the whole resource / VCALENDAR component
// not removing it causes Lightning to remove the exception itself
2010-02-17 14:29:28 +01:00
if (( $e = array_search ( $recurrence [ 'recurrence' ], $exceptions )) !== false )
2009-12-27 05:21:33 +01:00
{
2010-02-17 14:29:28 +01:00
unset ( $exceptions [ $e ]);
2009-12-27 05:21:33 +01:00
}
continue ; // nothing to change
}
2017-03-31 17:38:02 +02:00
// add alarms from master to recurrences, as clients otherwise have no alarms on virtual exceptions
if ( $master && $master [ 'alarm' ])
2015-06-25 22:39:53 +02:00
{
2017-03-31 17:38:02 +02:00
$recurrence [ 'alarm' ] = $master [ 'alarm' ];
2015-06-25 22:39:53 +02:00
}
2009-12-27 05:21:33 +01:00
// now we need to check if this recurrence is an exception
2015-10-09 19:37:01 +02:00
if ( ! $expand && $master && $master [ 'participants' ] == $recurrence [ 'participants' ])
2009-12-27 05:21:33 +01:00
{
//error_log('NO exception: '.array2string($recurrence));
unset ( $events [ $k ]); // no exception --> remove it
continue ;
}
2010-02-17 14:29:28 +01:00
// this is a virtual exception now (no extra event/cal_id in DB)
2009-12-27 05:21:33 +01:00
//error_log('virtual exception: '.array2string($recurrence));
$recurrence [ 'recurrence' ] = $recurrence [ 'start' ];
2015-10-09 19:37:01 +02:00
if ( $master ) $recurrence [ 'reference' ] = $master [ 'id' ];
2009-12-27 05:21:33 +01:00
$recurrence [ 'recur_type' ] = MCAL_RECUR_NONE ; // is set, as this is a copy of the master
// not for included exceptions (Lightning): $master['recur_exception'][] = $recurrence['start'];
}
2014-11-10 21:04:37 +01:00
// only add master if we are not expanding and current user participates in master (and not just some exceptions)
2019-01-21 12:55:18 +01:00
// also need to add master, if we have no (other) $events eg. birthday noone every accepted/rejected
if ( ! $expand && $master && ( ! $user || self :: isParticipant ( $master , $user ) || ! $events ))
2012-01-24 06:04:35 +01:00
{
$events = array_merge ( array ( $master ), $events );
}
2009-12-27 05:21:33 +01:00
return $events ;
}
2008-05-08 22:31:32 +02:00
2015-01-27 19:12:10 +01:00
/**
* Check if $user is a participant of given $event incl . group - invitations
*
* @ param array $event
* @ param int | string $user
* @ return boolean
*/
public static function isParticipant ( array $event , $user )
{
return isset ( $event [ 'participants' ][ $user ]) ||
2019-01-21 12:55:18 +01:00
// for users and group-invitations we need to check memberships of $user too
$user > 0 && array_intersect ( array_keys ( $event [ 'participants' ]),
( array ) $GLOBALS [ 'egw' ] -> accounts -> memberships ( $user , true ));
2015-01-27 19:12:10 +01:00
}
2008-05-08 22:31:32 +02:00
/**
* Handle put request for an event
*
* @ param array & $options
* @ param int $id
2015-06-25 22:39:53 +02:00
* @ param int $user = null account_id of owner , default null
* @ param string $prefix = null user prefix from path ( eg . / ralf from / ralf / addressbook )
2008-05-08 22:31:32 +02:00
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
2023-07-21 17:41:37 +02:00
function put ( & $options , $id , $user = null , $prefix = null , $method = 'PUT' )
2008-05-08 22:31:32 +02:00
{
2010-02-23 19:19:12 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " ( $id , $user ) " . print_r ( $options , true ));
2010-10-26 11:35:44 +02:00
if ( ! $prefix ) $user = null ; // /infolog/ does not imply setting the current user (for new entries it's done anyway)
2019-02-21 17:00:00 +01:00
// work around missing handling / racecondition in Lightning, if event already exists on server,
// but Lightning has not yet synced with the server: Lightning just retries the PUT, not GETing the event
// --> for now we ignore the If-None-Match: "*" as the lesser of two evils ;)
if ( self :: get_agent () === 'lightning' && isset ( $_SERVER [ 'HTTP_IF_NONE_MATCH' ]) &&
in_array ( $_SERVER [ 'HTTP_IF_NONE_MATCH' ], array ( '*' , '"*"' )))
{
unset ( $_SERVER [ 'HTTP_IF_NONE_MATCH' ]);
$workaround_lightning_if_none_match = true ;
}
2011-10-23 09:59:05 +02:00
// fix for iCal4OL using WinHTTP only supporting a certain header length
if ( isset ( $_SERVER [ 'HTTP_IF_SCHEDULE' ]) && ! isset ( $_SERVER [ 'HTTP_IF_SCHEDULE_TAG_MATCH' ]))
{
$_SERVER [ 'HTTP_IF_SCHEDULE_TAG_MATCH' ] = $_SERVER [ 'HTTP_IF_SCHEDULE' ];
}
2023-08-10 15:11:35 +02:00
// Thunderbird or Lightning: for "412 Precondition Failed" update only participant data, as TB offers user to overwrite server state
// CalDAVSynchronizer: does NOT handle 412 well and fails the complete sync, updating participant data only is the better option
$handle_etag_failure_with_partial_update = in_array ( self :: get_agent (), [ 'thunderbird' , 'lightning' , 'caldavsynchronizer' ]);
2010-05-18 12:03:21 +02:00
$return_no_access = true ; // as handled by importVCal anyway and allows it to set the status for participants
2011-10-20 22:10:04 +02:00
$oldEvent = $this -> _common_get_put_delete ( 'PUT' , $options , $id , $return_no_access ,
2023-08-10 15:11:35 +02:00
isset ( $_SERVER [ 'HTTP_IF_SCHEDULE_TAG_MATCH' ]), ! $handle_etag_failure_with_partial_update ); // dont fail with 412 Precondition Failed in that case
if ( $oldEvent === '412 Precondition Failed' && $handle_etag_failure_with_partial_update )
{
$oldEvent = $this -> read ( $id );
$this -> caldav -> log ( " Handing 412 Precondition Failed for Thunderbird by only updating participant data! " );
}
elseif ( ! is_null ( $oldEvent ) && ! is_array ( $oldEvent ))
2010-03-07 00:06:43 +01:00
{
2010-06-29 11:19:25 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . ': ' . print_r ( $oldEvent , true ) . function_backtrace ());
2010-03-07 00:06:43 +01:00
return $oldEvent ;
}
2023-08-10 15:11:35 +02:00
else
{
// set it to false, as we have no precondition failure
$handle_etag_failure_with_partial_update = false ;
}
2009-07-14 21:51:03 +02:00
2016-05-01 19:47:59 +02:00
if ( is_null ( $oldEvent ) && ( $user >= 0 && ! $this -> bo -> check_perms ( Acl :: ADD , 0 , $user ) ||
2013-01-24 16:32:56 +01:00
// if we require an extra invite grant, we fail if that does not exist (bind privilege is not given in that case)
$this -> bo -> require_acl_invite && $user && $user != $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] &&
! $this -> bo -> check_acl_invite ( $user )))
2008-05-08 22:31:32 +02:00
{
2010-03-07 00:06:43 +01:00
// we have no add permission on this user's calendar
2010-10-26 11:35:44 +02:00
// ToDo: create event in current users calendar and invite only $user
if ( $this -> debug ) error_log ( __METHOD__ . " (,, $user ) we have not enough rights on this calendar " );
2010-03-07 00:06:43 +01:00
return '403 Forbidden' ;
2008-05-08 22:31:32 +02:00
}
2010-03-07 00:06:43 +01:00
2008-11-03 10:36:20 +01:00
$handler = $this -> _get_handler ();
2010-03-07 00:06:43 +01:00
$vCalendar = htmlspecialchars_decode ( $options [ 'content' ]);
2010-06-14 09:51:28 +02:00
$charset = null ;
if ( ! empty ( $options [ 'content_type' ]))
{
$content_type = explode ( ';' , $options [ 'content_type' ]);
if ( count ( $content_type ) > 1 )
{
array_shift ( $content_type );
foreach ( $content_type as $attribute )
{
list ( $key , $value ) = explode ( '=' , $attribute );
2018-12-18 16:20:01 +01:00
switch ( strtolower ( trim ( $key )))
2010-06-14 09:51:28 +02:00
{
case 'charset' :
2018-12-18 16:20:01 +01:00
$charset = strtoupper ( trim ( $value ));
2010-06-14 09:51:28 +02:00
}
}
2010-10-26 11:35:44 +02:00
}
2010-06-14 09:51:28 +02:00
}
2010-02-17 14:29:28 +01:00
2024-03-13 16:25:22 +01:00
// if path not found, check the UID and return "403 Forbidden" if event is deleted or user has not rights to event with same UID
if ( ! isset ( $oldEvent ) && ( $events = $handler -> icaltoegw ( $vCalendar )) &&
( $oldEvents = $this -> bo -> read ([ 'cal_uid' => $events [ 0 ][ 'uid' ], 'cal_reference=0' ], null , false , 'server' )) !== null )
{
foreach ( $oldEvents as $oldEvent )
{
if ( empty ( $oldEvent [ 'deleted' ]))
{
break ;
}
}
if ( ! empty ( $oldEvent [ 'deleted' ]))
{
$this -> caldav -> log ( " Event with UID=' { $events [ 0 ][ 'uid' ] } ' has already been deleted! " );
return '403 Forbidden' ;
}
// case user has no edit-rights for $oldEvent is handled below
}
2010-03-07 00:06:43 +01:00
if ( is_array ( $oldEvent ))
2010-02-11 21:50:35 +01:00
{
2010-03-07 00:06:43 +01:00
$eventId = $oldEvent [ 'id' ];
2011-10-20 22:10:04 +02:00
2012-02-21 21:04:45 +01:00
//client specified a CalDAV Scheduling schedule-tag AND an etag If-Match precondition
if ( $this -> use_schedule_tag && isset ( $_SERVER [ 'HTTP_IF_SCHEDULE_TAG_MATCH' ]) &&
isset ( $_SERVER [ 'HTTP_IF_MATCH' ]))
{
if ( $oldEvent [ 'owner' ] == $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ])
{
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( " Both If-Match and If-Schedule-Tag-Match header given: If-Schedule-Tag-Match ignored for event owner! " );
2012-02-21 21:04:45 +01:00
unset ( $_SERVER [ 'HTTP_IF_SCHEDULE_TAG_MATCH' ]);
}
else
{
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( " Both If-Match and If-Schedule-Tag-Match header given: If-Schedule-Tag-Match takes precedence for participants! " );
2012-02-21 21:04:45 +01:00
}
}
2012-10-23 13:35:07 +02:00
// check CalDAV Scheduling schedule-tag precondition
2011-11-08 22:09:06 +01:00
if ( $this -> use_schedule_tag && isset ( $_SERVER [ 'HTTP_IF_SCHEDULE_TAG_MATCH' ]))
2011-10-20 22:10:04 +02:00
{
$schedule_tag_match = $_SERVER [ 'HTTP_IF_SCHEDULE_TAG_MATCH' ];
if ( $schedule_tag_match [ 0 ] == '"' ) $schedule_tag_match = substr ( $schedule_tag_match , 1 , - 1 );
2015-06-25 22:39:53 +02:00
$schedule_tag = null ;
2011-10-20 22:10:04 +02:00
$this -> get_etag ( $oldEvent , $schedule_tag );
if ( $schedule_tag_match !== $schedule_tag )
{
2023-08-10 15:11:35 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " (,, $user ) schedule_tag mismatch: given ' $schedule_tag_match ' != ' $schedule_tag ' " );
2013-09-25 09:09:44 +02:00
// honor Prefer: return=representation for 412 too (no need for client to explicitly reload)
$this -> check_return_representation ( $options , $id , $user );
2011-10-20 22:10:04 +02:00
return '412 Precondition Failed' ;
}
2012-10-23 13:35:07 +02:00
}
// if no edit-rights (aka no organizer), update only attendee stuff: status and alarms
2018-10-09 13:14:36 +02:00
if ( ! $this -> check_access ( Acl :: EDIT , $oldEvent ) ||
2023-08-10 15:11:35 +02:00
// only update participant data, if precondition is NOT meet
$handle_etag_failure_with_partial_update ||
2019-02-21 17:00:00 +01:00
// we ignored Lightings If-None-Match: "*" --> do not overwrite event, just change status
! empty ( $workaround_lightning_if_none_match ))
2012-10-23 13:35:07 +02:00
{
2013-01-15 14:12:10 +01:00
$user_and_memberships = $GLOBALS [ 'egw' ] -> accounts -> memberships ( $user , true );
$user_and_memberships [] = $user ;
2015-10-12 13:55:20 +02:00
if ( ! array_intersect ( array_keys ( $oldEvent [ 'participants' ]), $user_and_memberships ) &&
// above can be true, if current user is not in master but just a recurrence
( ! $oldEvent [ 'recur_type' ] || ! ( $series = self :: get_series ( $oldEvent [ 'uid' ], $this -> bo ))))
2012-10-23 13:35:07 +02:00
{
if ( $this -> debug ) error_log ( __METHOD__ . " (,, $user ) user $user is NOT an attendee! " );
2024-01-16 14:09:56 +01:00
$ignore_acl = ! empty ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'caldav_party_crasher_regexp' ]) &&
preg_match ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'caldav_party_crasher_regexp' ], $email = Api\Accounts :: id2name ( $user , 'account_email' ));
if ( ! $ignore_acl )
{
$this -> caldav -> log ( " Returning '403 Forbidden' as # $user is NOT a participant of the event! " );
return '403 Forbidden' ;
}
$this -> caldav -> log ( " Allowing user # $user because email ' $email ' matches ' { $GLOBALS [ 'egw_info' ][ 'server' ][ 'caldav_party_crasher_regexp' ] } ' " );
2012-10-23 13:35:07 +02:00
}
2011-10-20 22:10:04 +02:00
// update only participant status and alarms of current user
2012-10-23 13:35:07 +02:00
if (( $events = $handler -> icaltoegw ( $vCalendar )))
2011-10-20 22:10:04 +02:00
{
2012-08-15 17:27:11 +02:00
$modified = 0 ;
2020-08-21 13:59:15 +02:00
$master = null ;
2012-08-15 17:27:11 +02:00
foreach ( $events as $n => $event )
2011-10-20 22:10:04 +02:00
{
2024-01-16 14:09:56 +01:00
// for recurrences of event series, we need to read correct recurrence (or if series master is no first event)
2015-10-12 13:55:20 +02:00
if ( $event [ 'recurrence' ] || $n && ! $event [ 'recurrence' ] || isset ( $series ))
2012-08-15 17:27:11 +02:00
{
// first try reading (virtual and real) exceptions
if ( ! isset ( $series ))
{
$series = self :: get_series ( $event [ 'uid' ], $this -> bo );
//foreach($series as $s => $sEvent) error_log("series[$s]: ".array2string($sEvent));
}
foreach ( $series as $oldEvent )
{
if ( $oldEvent [ 'recurrence' ] == $event [ 'recurrence' ]) break ;
}
// if no exception found, check if it might be just a recurrence (no exception)
2015-10-12 13:55:20 +02:00
if ( $event [ 'recurrence' ] && $oldEvent [ 'recurrence' ] != $event [ 'recurrence' ])
2012-08-15 17:27:11 +02:00
{
if ( ! ( $oldEvent = $this -> bo -> read ( $eventId , $event [ 'recurrence' ], true )) ||
// virtual exceptions have recurrence=0 and recur_date=recurrence (series master or real exceptions have recurence=0)
! ( $oldEvent [ 'recur_date' ] == $event [ 'recurrence' ] || ! $event [ 'recurrence' ] && ! $oldEvent [ 'recurrence' ]))
{
// if recurrence not found --> log it and continue with other recurrence
2016-05-01 19:47:59 +02:00
$this -> caldav -> log ( __METHOD__ . " (,, $user ) could NOT find recurrence= $event[recurrence] = " . Api\DateTime :: to ( $event [ 'recurrence' ]) . ' of event series! event=' . array2string ( $event ));
2012-08-15 17:27:11 +02:00
continue ;
}
}
}
2016-05-01 19:47:59 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " (, $id , $user , ' $prefix ') eventId= $eventId ( $oldEvent[id] ), user= $user , old-status=' { $oldEvent [ 'participants' ][ $user ] } ', new-status=' { $event [ 'participants' ][ $user ] } ', recurrence= $event[recurrence] = " . Api\DateTime :: to ( $event [ 'recurrence' ]) . " , event= " . array2string ( $event ));
2013-01-25 18:52:28 +01:00
if ( isset ( $event [ 'participants' ]) && isset ( $event [ 'participants' ][ $user ]) &&
$event [ 'participants' ][ $user ] !== $oldEvent [ 'participants' ][ $user ])
2011-11-08 22:09:06 +01:00
{
2012-08-15 17:27:11 +02:00
if ( ! $this -> bo -> set_status ( $oldEvent [ 'id' ], $user , $event [ 'participants' ][ $user ],
// real (not virtual) exceptions use recurrence 0 in egw_cal_user.cal_recurrence!
2024-01-16 14:09:56 +01:00
$recurrence = $eventId == $oldEvent [ 'id' ] ? $event [ 'recurrence' ] : 0 ,
$ignore_acl ))
2011-11-08 22:09:06 +01:00
{
2016-05-01 19:47:59 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " (,, $user ) failed to set_status( $oldEvent[id] , $user , ' { $event [ 'participants' ][ $user ] } ', $recurrence = " . Api\DateTime :: to ( $recurrence ) . ')' );
2011-11-08 22:09:06 +01:00
return '403 Forbidden' ;
}
else
{
2012-08-15 17:27:11 +02:00
++ $modified ;
2016-05-01 19:47:59 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " () set_status( $oldEvent[id] , $user , { $event [ 'participants' ][ $user ] } , $recurrence = " . Api\DateTime :: to ( $recurrence ) . ')' );
2011-11-08 22:09:06 +01:00
}
}
// import alarms, if given and changed
if (( array ) $event [ 'alarm' ] !== ( array ) $oldEvent [ 'alarm' ])
{
2015-06-25 22:39:53 +02:00
$event [ 'id' ] = $oldEvent [ 'id' ];
2020-08-21 13:59:15 +02:00
if ( isset ( $master ) && $event [ 'id' ] == $master [ 'id' ]) // only for pseudo exceptions
{
$modified += $handler -> sync_alarms ( $event , ( array ) $oldEvent [ 'alarm' ], $user , $master );
}
else
{
$modified += $handler -> sync_alarms ( $event , ( array ) $oldEvent [ 'alarm' ], $user );
}
2011-10-20 22:10:04 +02:00
}
2020-08-21 13:59:15 +02:00
if ( ! isset ( $master ) && ! $event [ 'recurrence' ]) $master = $event ;
2011-10-20 22:10:04 +02:00
}
2023-07-21 17:41:37 +02:00
if ( ! $modified ) // NO modification, or none we understood --> log it and return Ok: "204 No Content"
2012-08-15 17:27:11 +02:00
{
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( __METHOD__ . " (,, $user ) NO changes for current user events= " . array2string ( $events ) . ', old-event=' . array2string ( $oldEvent ));
2012-08-15 17:27:11 +02:00
}
2012-10-29 13:23:17 +01:00
$this -> put_response_headers ( $eventId , $options [ 'path' ], '204 No Content' , self :: $path_attr == 'caldav_name' );
2011-10-20 22:10:04 +02:00
return '204 No Content' ;
}
2024-01-16 14:09:56 +01:00
else
{
$this -> caldav -> log ( " Could NOT parse any event(s) from iCal --> returning 400 Bad Request! " );
return '400 Bad Request' ;
}
2011-11-08 22:09:06 +01:00
if ( $this -> debug && ! isset ( $events )) error_log ( __METHOD__ . " (,, $user ) only schedule-tag given for event without participants (only calendar owner) --> handle as regular PUT " );
2011-10-20 22:10:04 +02:00
}
2010-03-07 00:06:43 +01:00
if ( $return_no_access )
{
$retval = true ;
}
else
{
2011-03-05 11:21:32 +01:00
$retval = '204 No Content' ;
// lightning will pop up the alarm, as long as the Sequence (etag) does NOT change
// --> update the etag alone, if user has no edit rights
2016-05-01 19:47:59 +02:00
if ( $this -> agent == 'lightning' && ! $this -> check_access ( Acl :: EDIT , $oldEvent ) &&
2011-03-05 11:21:32 +01:00
isset ( $oldEvent [ 'participants' ][ $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ]]))
{
// just update etag in database
$GLOBALS [ 'egw' ] -> db -> update ( $this -> bo -> so -> cal_table , 'cal_etag=cal_etag+1' , array (
'cal_id' => $eventId ,
), __LINE__ , __FILE__ , 'calendar' );
}
2010-03-07 00:06:43 +01:00
}
}
2018-09-13 15:35:56 +02:00
// schedule tag with deleted event should not create a new entry, therefore returning 404 Not Found
elseif ( ! isset ( $oldEvent ) && isset ( $_SERVER [ 'HTTP_IF_SCHEDULE_TAG_MATCH' ]))
{
return '404 Not Found' ;
}
2010-03-07 00:06:43 +01:00
else
{
2011-04-06 21:26:10 +02:00
// new entry
$eventId = - 1 ;
$retval = '201 Created' ;
2010-02-11 21:50:35 +01:00
}
2010-02-17 14:29:28 +01:00
2023-07-21 17:41:37 +02:00
$type = null ;
if (( $is_json = Api\CalDAV :: isJSON ( $type )))
{
2023-09-18 14:13:09 +02:00
$event = Api\CalDAV\JsCalendar :: parseJsEvent ( $options [ 'content' ], $oldEvent ? ? [], $type , $method , $user );
2023-07-21 17:41:37 +02:00
$cal_id = $this -> bo -> save ( $event );
}
else
{
$cal_id = $handler -> importVCal ( $vCalendar , $eventId ,
self :: etag2value ( $this -> http_if_match ), false , 0 , $this -> caldav -> current_user_principal , $user , $charset , $id );
}
if ( ! $cal_id )
2008-05-08 22:31:32 +02:00
{
2011-11-08 22:09:06 +01:00
if ( $this -> debug ) error_log ( __METHOD__ . " (, $id ) eventId= $eventId : importVCal(' $options[content] ') returned " . array2string ( $cal_id ));
2010-05-18 16:41:22 +02:00
if ( $eventId && $cal_id === false )
{
// ignore import failures
$cal_id = $eventId ;
$retval = true ;
}
2011-11-08 22:09:06 +01:00
elseif ( $cal_id === 0 ) // etag failure
{
2013-09-25 09:09:44 +02:00
// honor Prefer: return=representation for 412 too (no need for client to explicitly reload)
$this -> check_return_representation ( $options , $id , $user );
2011-11-08 22:09:06 +01:00
return '412 Precondition Failed' ;
}
2010-05-18 16:41:22 +02:00
else
{
return '403 Forbidden' ;
}
2008-05-08 22:31:32 +02:00
}
2012-02-04 21:24:01 +01:00
// send evtl. necessary respose headers: Location, etag, ...
$this -> put_response_headers ( $cal_id , $options [ 'path' ], $retval , self :: $path_attr == 'caldav_name' );
2011-04-06 21:26:10 +02:00
return $retval ;
2008-05-08 22:31:32 +02:00
}
2010-02-11 21:50:35 +01:00
2010-05-17 16:20:34 +02:00
/**
* Handle post request for a schedule entry
*
* @ param array & $options
* @ param int $id
2015-06-25 22:39:53 +02:00
* @ param int $user = null account_id of owner , default null
2010-05-17 16:20:34 +02:00
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
function post ( & $options , $id , $user = null )
{
2010-06-29 11:19:25 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " ( $id , $user ) " . print_r ( $options , true ));
2010-10-26 11:35:44 +02:00
2011-09-22 17:22:52 +02:00
$vCalendar = htmlspecialchars_decode ( $options [ 'content' ]);
$charset = null ;
if ( ! empty ( $options [ 'content_type' ]))
2010-06-28 19:34:57 +02:00
{
2011-09-22 17:22:52 +02:00
$content_type = explode ( ';' , $options [ 'content_type' ]);
if ( count ( $content_type ) > 1 )
2010-06-29 11:19:25 +02:00
{
2011-09-22 17:22:52 +02:00
array_shift ( $content_type );
foreach ( $content_type as $attribute )
2010-06-29 11:19:25 +02:00
{
2011-09-22 17:22:52 +02:00
list ( $key , $value ) = explode ( '=' , $attribute );
2018-12-18 16:20:01 +01:00
switch ( strtolower ( trim ( $key )))
2010-06-29 11:19:25 +02:00
{
2011-09-22 17:22:52 +02:00
case 'charset' :
2018-12-18 16:20:01 +01:00
$charset = strtoupper ( trim ( $value ));
2010-06-29 11:19:25 +02:00
}
2010-10-26 11:35:44 +02:00
}
2010-06-29 11:19:25 +02:00
}
2011-09-22 17:22:52 +02:00
}
2010-10-26 11:35:44 +02:00
2011-09-22 17:22:52 +02:00
if ( substr ( $options [ 'path' ], - 8 ) == '/outbox/' )
{
if ( preg_match ( '/^METHOD:REQUEST(\r\n|\r|\n)(.*)^BEGIN:VFREEBUSY/ism' , $vCalendar ))
{
if ( $user != $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ])
{
error_log ( __METHOD__ . " () freebusy request only allowed to own outbox! " );
return '403 Forbidden' ;
}
// do freebusy request
return $this -> outbox_freebusy_request ( $vCalendar , $charset , $user , $options );
}
else
{
// POST to deliver an invitation, containing http headers:
// Originator: mailto:<organizer-email>
// Recipient: mailto:<attendee-email>
// --> currently we simply ignore these posts, as EGroupware does it's own notifications based on user preferences
return '204 No Content' ;
}
}
if ( preg_match ( '/^METHOD:(PUBLISH|REQUEST)(\r\n|\r|\n)(.*)^BEGIN:VEVENT/ism' , $options [ 'content' ]))
{
$handler = $this -> _get_handler ();
2021-03-28 20:48:55 +02:00
if (( $foundEvents = $handler -> iCalSearch ( $vCalendar , null , false , $charset )))
2010-06-29 11:19:25 +02:00
{
2015-06-25 22:39:53 +02:00
$id = array_shift ( $foundEvents );
list ( $eventId ) = explode ( ':' , $id );
2010-10-26 11:35:44 +02:00
2010-06-29 11:19:25 +02:00
if ( ! ( $cal_id = $handler -> importVCal ( $vCalendar , $eventId , null ,
2016-04-02 12:44:17 +02:00
false , 0 , $this -> caldav -> current_user_principal , $user , $charset )))
2010-06-29 11:19:25 +02:00
{
if ( $this -> debug ) error_log ( __METHOD__ . " () importVCal( $eventId ) returned false " );
}
2013-09-23 12:21:31 +02:00
header ( 'ETag: "' . $this -> get_etag ( $eventId ) . '"' );
2010-06-29 11:19:25 +02:00
}
2010-06-28 19:34:57 +02:00
}
2010-05-17 16:20:34 +02:00
return true ;
}
2011-09-22 17:22:52 +02:00
/**
* Handle outbox freebusy request
*
* @ param string $ical
* @ param string $charset of ical
* @ param int $user account_id of owner
* @ param array & $options
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
protected function outbox_freebusy_request ( $ical , $charset , $user , array & $options )
{
2015-06-25 22:39:53 +02:00
unset ( $options ); // not used, but required by function signature
2015-06-22 18:20:15 +02:00
$vcal = new Horde_Icalendar ();
2011-09-22 17:22:52 +02:00
if ( ! $vcal -> parsevCalendar ( $ical , 'VCALENDAR' , $charset ))
{
return '400 Bad request' ;
}
$version = $vcal -> getAttribute ( 'VERSION' );
//echo $ical."\n";
$handler = $this -> _get_handler ();
$handler -> setSupportedFields ( 'groupdav' );
$handler -> calendarOwner = $handler -> user = 0 ; // to NOT default owner/organizer to something
if ( ! ( $component = $vcal -> getComponent ( 0 )) ||
2016-04-02 12:44:17 +02:00
! ( $event = $handler -> vevent2egw ( $component , $version , $handler -> supportedFields , $this -> caldav -> current_user_principal , 'Horde_Icalendar_Vfreebusy' )))
2011-09-22 17:22:52 +02:00
{
return '400 Bad request' ;
}
if ( $event [ 'owner' ] != $user )
{
2016-04-02 12:44:17 +02:00
$this -> caldav -> log ( __METHOD__ . " (' $ical ',, $user ) ORGANIZER is NOT principal! " );
2011-09-22 17:22:52 +02:00
return '403 Forbidden' ;
}
//print_r($event);
$organizer = $component -> getAttribute ( 'ORGANIZER' );
2011-09-23 14:04:21 +02:00
$attendees = ( array ) $component -> getAttribute ( 'ATTENDEE' );
2011-09-22 17:22:52 +02:00
// X-CALENDARSERVER-MASK-UID specifies to exclude given event from busy-time
2015-06-25 22:39:53 +02:00
$mask_uid = $component -> getAttributeDefault ( 'X-CALENDARSERVER-MASK-UID' , null );
2011-09-22 17:22:52 +02:00
header ( 'Content-type: text/xml; charset=UTF-8' );
$xml = new XMLWriter ;
$xml -> openMemory ();
2012-10-02 07:17:32 +02:00
$xml -> setIndent ( true );
2011-09-22 17:22:52 +02:00
$xml -> startDocument ( '1.0' , 'UTF-8' );
2016-04-02 12:44:17 +02:00
$xml -> startElementNs ( 'C' , 'schedule-response' , Api\CalDAV :: CALDAV );
2011-09-22 17:22:52 +02:00
2021-11-15 11:55:24 +01:00
foreach ( array_keys ( $event [ 'participants' ] ? ? []) as $uid )
2011-09-22 17:22:52 +02:00
{
$xml -> startElementNs ( 'C' , 'response' , null );
$xml -> startElementNs ( 'C' , 'recipient' , null );
$xml -> writeElementNs ( 'D' , 'href' , 'DAV:' , $attendee = array_shift ( $attendees ));
$xml -> endElement (); // recipient
2011-10-17 17:36:28 +02:00
$xml -> writeElementNs ( 'C' , 'request-status' , null , '2.0;Success' );
$xml -> writeElementNs ( 'C' , 'calendar-data' , null ,
$handler -> freebusy ( $uid , $event [ 'end' ], true , 'utf-8' , $event [ 'start' ], 'REPLY' , array (
'UID' => $event [ 'uid' ],
'ORGANIZER' => $organizer ,
'ATTENDEE' => $attendee ,
) + ( empty ( $mask_uid ) || ! is_string ( $mask_uid ) ? array () : array (
'X-CALENDARSERVER-MASK-UID' => $mask_uid ,
))));
2011-09-22 17:22:52 +02:00
$xml -> endElement (); // response
}
$xml -> endElement (); // schedule-response
$xml -> endDocument ();
echo $xml -> outputMemory ();
return true ;
}
2011-09-22 21:49:01 +02:00
/**
* Handle free - busy - query report
*
* @ param string $path
* @ param array $options
* @ param int $user account_id
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
function free_busy_report ( $path , $options , $user )
{
2015-06-25 22:39:53 +02:00
unset ( $path ); // unused, but required by function signature
2016-05-01 19:47:59 +02:00
if ( ! $this -> bo -> check_perms ( calendar_bo :: ACL_FREEBUSY , 0 , $user ))
2011-09-22 21:49:01 +02:00
{
return '403 Forbidden' ;
}
foreach ( $options [ 'other' ] as $filter )
{
if ( $filter [ 'name' ] == 'time-range' )
{
$start = $this -> vCalendar -> _parseDateTime ( $filter [ 'attrs' ][ 'start' ]);
$end = $this -> vCalendar -> _parseDateTime ( $filter [ 'attrs' ][ 'end' ]);
}
}
$handler = $this -> _get_handler ();
header ( 'Content-Type: text/calendar' );
echo $handler -> freebusy ( $user , $end , true , 'utf-8' , $start , 'REPLY' , array ());
2016-05-01 19:47:59 +02:00
exit (); // otherwise we get a 207 multistatus, not 200 Ok
2011-09-22 21:49:01 +02:00
}
2011-09-22 20:46:16 +02:00
/**
* Return priviledges for current user , default is read and read - current - user - privilege - set
*
* Reimplemented to add read - free - busy and schedule - deliver privilege
*
* @ param string $path path of collection
2015-06-25 22:39:53 +02:00
* @ param int $user = null owner of the collection , default current user
2011-09-22 20:46:16 +02:00
* @ return array with privileges
*/
public function current_user_privileges ( $path , $user = null )
{
2012-10-02 07:17:32 +02:00
$privileges = parent :: current_user_privileges ( $path , $user );
//error_log(__METHOD__."('$path', $user) parent gave ".array2string($privileges));
2011-09-22 20:46:16 +02:00
2016-05-01 19:47:59 +02:00
if ( $this -> bo -> check_perms ( calendar_bo :: ACL_FREEBUSY , 0 , $user ))
2011-09-22 20:46:16 +02:00
{
2016-04-02 12:44:17 +02:00
$privileges [ 'read-free-busy' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'read-free-busy' , '' );
2011-09-22 20:46:16 +02:00
if ( substr ( $path , - 8 ) == '/outbox/' && $this -> bo -> check_acl_invite ( $user ))
{
2016-04-02 12:44:17 +02:00
$privileges [ 'schedule-send' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'schedule-send' , '' );
2011-09-22 20:46:16 +02:00
}
}
if ( substr ( $path , - 7 ) == '/inbox/' && $this -> bo -> check_acl_invite ( $user ))
{
2016-04-02 12:44:17 +02:00
$privileges [ 'schedule-deliver' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'schedule-deliver' , '' );
2011-09-22 20:46:16 +02:00
}
2016-05-01 19:47:59 +02:00
// remove bind privilege on other users or groups calendars, if calendar Api\Config require_acl_invite is set
2013-01-24 13:54:08 +01:00
// and current user has no invite grant
if ( $user && $user != $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] && isset ( $privileges [ 'bind' ]) &&
2013-01-24 16:32:56 +01:00
! $this -> bo -> check_acl_invite ( $user ))
2013-01-24 13:54:08 +01:00
{
unset ( $privileges [ 'bind' ]);
}
2012-10-02 07:17:32 +02:00
//error_log(__METHOD__."('$path', $user) returning ".array2string($privileges));
return $privileges ;
2011-09-22 20:46:16 +02:00
}
2009-12-27 05:21:33 +01:00
/**
* Fix event series with exceptions , called by calendar_ical :: importVCal () :
* a ) only series master = first event got cal_id from URL
* b ) exceptions need to be checked if they are already in DB or new
* c ) recurrence - id of ( real not virtual ) exceptions need to be re - added to master
2010-02-11 21:50:35 +01:00
*
2009-12-27 05:21:33 +01:00
* @ param array & $events
*/
static function fix_series ( array & $events )
{
$bo = new calendar_boupdate ();
2010-02-11 21:50:35 +01:00
2009-12-27 05:21:33 +01:00
// get array with orginal recurrences indexed by recurrence-id
2010-02-17 14:29:28 +01:00
$org_recurrences = $exceptions = array ();
foreach ( self :: get_series ( $events [ 0 ][ 'uid' ], $bo ) as $k => $event )
2009-12-27 05:21:33 +01:00
{
2010-02-17 14:29:28 +01:00
if ( ! $k ) $master = $event ;
2009-12-27 05:21:33 +01:00
if ( $event [ 'recurrence' ])
{
$org_recurrences [ $event [ 'recurrence' ]] = $event ;
}
}
// assign cal_id's to already existing recurrences and evtl. re-add recur_exception to master
2010-02-17 14:29:28 +01:00
foreach ( $events as $k => & $recurrence )
2009-12-27 05:21:33 +01:00
{
2010-02-17 14:29:28 +01:00
if ( ! $recurrence [ 'recurrence' ])
{
// master
$recurrence [ 'id' ] = $master [ 'id' ];
$master =& $events [ $k ];
continue ;
}
2010-02-11 21:50:35 +01:00
2009-12-27 05:21:33 +01:00
// from now on we deal with exceptions
2021-10-19 09:18:00 +02:00
$org_recurrence = isset ( $recurrence [ 'recurrence' ]) ? $org_recurrences [ $recurrence [ 'recurrence' ]] : null ;
2009-12-27 05:21:33 +01:00
if ( isset ( $org_recurrence )) // already existing recurrence
{
2010-05-19 18:13:37 +02:00
//error_log(__METHOD__.'() setting id #'.$org_recurrence['id']).' for '.$recurrence['recurrence'].' = '.date('Y-m-d H:i:s',$recurrence['recurrence']);
2009-12-27 05:21:33 +01:00
$recurrence [ 'id' ] = $org_recurrence [ 'id' ];
2010-02-11 21:50:35 +01:00
2009-12-27 05:21:33 +01:00
// re-add (non-virtual) exceptions to master's recur_exception
if ( $recurrence [ 'id' ] != $master [ 'id' ])
{
2010-05-19 18:13:37 +02:00
//error_log(__METHOD__.'() re-adding recur_exception '.$recurrence['recurrence'].' = '.date('Y-m-d H:i:s',$recurrence['recurrence']));
2010-02-17 14:29:28 +01:00
$exceptions [] = $recurrence [ 'recurrence' ];
2009-12-27 05:21:33 +01:00
}
// remove recurrence to be able to detect deleted exceptions
unset ( $org_recurrences [ $recurrence [ 'recurrence' ]]);
}
}
2010-02-17 14:29:28 +01:00
$master [ 'recur_exception' ] = array_merge ( $exceptions , $master [ 'recur_exception' ]);
2009-12-27 05:21:33 +01:00
// delete not longer existing recurrences
foreach ( $org_recurrences as $org_recurrence )
{
if ( $org_recurrence [ 'id' ] != $master [ 'id' ]) // non-virtual recurrence
{
2010-02-26 13:37:07 +01:00
//error_log(__METHOD__.'() deleting #'.$org_recurrence['id']);
2009-12-27 05:21:33 +01:00
$bo -> delete ( $org_recurrence [ 'id' ]); // might fail because of permissions
}
else // virtual recurrence
{
2010-05-19 18:13:37 +02:00
//error_log(__METHOD__.'() delete virtual exception '.$org_recurrence['recurrence'].' = '.date('Y-m-d H:i:s',$org_recurrence['recurrence']));
2010-02-26 13:37:07 +01:00
$bo -> update_status ( $master , $org_recurrence , $org_recurrence [ 'recurrence' ]);
2009-12-27 05:21:33 +01:00
}
}
2010-02-26 13:37:07 +01:00
//foreach($events as $n => $event) error_log(__METHOD__." $n after: ".array2string($event));
2009-12-27 05:21:33 +01:00
}
2008-05-08 22:31:32 +02:00
/**
* Handle delete request for an event
*
2008-05-17 15:00:34 +02:00
* If current user has no right to delete the event , but is an attendee , we reject the event for him .
*
2009-12-27 05:21:33 +01:00
* @ todo remove ( non - virtual ) exceptions , if series master gets deleted
2008-05-08 22:31:32 +02:00
* @ param array & $options
* @ param int $id
2018-10-09 13:14:36 +02:00
* @ param int $user account_id of collection owner
2008-05-08 22:31:32 +02:00
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
2018-10-09 13:14:36 +02:00
function delete ( & $options , $id , $user )
2008-05-08 22:31:32 +02:00
{
2011-09-22 17:22:52 +02:00
if ( strpos ( $options [ 'path' ], '/inbox/' ) !== false )
{
return true ; // simply ignore DELETE in inbox for now
}
2010-05-17 16:20:34 +02:00
$return_no_access = true ; // to allow to check if current use is a participant and reject the event for him
2020-03-04 20:17:34 +01:00
$event = $this -> _common_get_put_delete ( 'DELETE' , $options , $id , $return_no_access );
2024-03-13 16:25:22 +01:00
/* user has delete - rights , check if we have an external organizer and more participants
if ( $event && $return_no_access && count ( $event [ 'participants' ]) > 2 )
{
foreach ( $event [ 'participants' ] as $uid => $status )
{
$quantity = $role = null ;
calendar_so :: split_status ( $status , $quantity , $role );
if ( ! is_numeric ( $uid ) && $role == 'CHAIR' ) break ;
}
if ( ! ( ! is_numeric ( $uid ) && $role == 'CHAIR' ))
{
$return_no_access = false ; // only set status rejected, but do NOT delete the event
}
} */
2020-03-04 20:17:34 +01:00
// no event found --> 404 Not Found
if ( ! is_array ( $event ))
{
$ret = $event ;
error_log ( " _common_get_put_delete('DELETE', ..., $id ) user= $user , return_no_access= " . array2string ( $return_no_access ) . " returned " . array2string ( $event ));
}
// Work around problems with Outlook CalDAV Synchronizer (https://caldavsynchronizer.org/)
// - sends a DELETE to reject a meeting request --> deletes event for all participants, if user has delete rights from the organizer
// --> only set status for everyone else but the organizer
// OR no delete rights and deleting an event in someone else calendar --> check if calendar owner is a participant --> reject him
elseif (( ! $return_no_access || ( self :: get_agent () === 'caldavsynchronizer' && $event [ 'owner' ] != $user )) &&
// check if current user has edit rights for calendar of $user, can change status / reject invitation for him
$this -> bo -> check_perms ( Acl :: EDIT , 0 , $user ))
{
// check if user is a participant or one of the groups he is a member of --> reject the meeting request
$ret = '403 Forbidden' ;
$memberships = $GLOBALS [ 'egw' ] -> accounts -> memberships ( $user , true );
foreach ( array_keys ( $event [ 'participants' ]) as $uid )
2008-05-17 15:00:34 +02:00
{
2020-03-04 20:17:34 +01:00
if ( $user == $uid || in_array ( $uid , $memberships ))
2011-10-05 09:39:11 +02:00
{
2020-03-04 20:17:34 +01:00
$this -> bo -> set_status ( $event , $user , 'R' );
$ret = true ;
break ;
2011-10-05 09:39:11 +02:00
}
2008-05-17 15:00:34 +02:00
}
2020-03-04 20:17:34 +01:00
}
// current user has no delete rights for event --> reject invitation, if he is a participant
elseif ( ! $return_no_access )
{
// check if current user is a participant or one of the groups he is a member of --> reject the meeting request
$ret = '403 Forbidden' ;
$memberships = $GLOBALS [ 'egw' ] -> accounts -> memberships ( $this -> bo -> user , true );
foreach ( array_keys ( $event [ 'participants' ]) as $uid )
2011-10-05 09:39:11 +02:00
{
2020-03-04 20:17:34 +01:00
if ( $this -> bo -> user == $uid || in_array ( $uid , $memberships ))
{
$this -> bo -> set_status ( $event , $this -> bo -> user , 'R' );
$ret = true ;
break ;
}
2011-10-05 09:39:11 +02:00
}
}
2020-03-04 20:17:34 +01:00
// we have delete rights on the event and (try to) delete it
2011-10-05 09:39:11 +02:00
else
{
$ret = $this -> bo -> delete ( $event [ 'id' ]);
2020-03-04 20:17:34 +01:00
if ( ! $ret ) { error_log ( " delete( $event[id] ) returned FALSE " ); $ret = '400 Failed to delete event' ;}
2008-05-08 22:31:32 +02:00
}
2011-10-05 09:39:11 +02:00
if ( $this -> debug ) error_log ( __METHOD__ . " (, $id ) return_no_access= $return_no_access , event[participants]= " . array2string ( is_array ( $event ) ? $event [ 'participants' ] : null ) . " , user= { $this -> bo -> user } --> return " . array2string ( $ret ));
return $ret ;
2008-05-08 22:31:32 +02:00
}
/**
* Read an entry
*
2011-10-04 16:18:35 +02:00
* We have to make sure to not return or even consider in read deleted events , as the might have
* the same UID and / or caldav_name as not deleted events and would block access to valid entries
*
2011-04-06 21:26:10 +02:00
* @ param string | id $id
2024-03-13 16:25:22 +01:00
* @ return array | boolean array with entry , false if no read rights or deleted , null if $id does not exist
2008-05-08 22:31:32 +02:00
*/
function read ( $id )
{
2011-04-06 21:26:10 +02:00
if ( strpos ( $column = self :: $path_attr , '_' ) === false ) $column = 'cal_' . $column ;
2024-03-13 16:25:22 +01:00
$event = $this -> bo -> read ( array ( $column => $id , 'cal_reference=0' ), null , true ,
2023-07-24 17:08:05 +02:00
$date_format = Api\CalDAV :: isJSON () ? 'object' : 'server' );
2024-03-13 16:25:22 +01:00
// read with array as 1. param, returns an array of events!
// as we no longer return only NOT-deleted events, there might be more
if ( $event )
{
foreach ( $event as $event )
{
if ( empty ( $event [ 'cal_deleted' ])) break ;
}
// the above prefers a NOT deleted event over deleted noes, thought all might be deleted
if ( ! empty ( $event [ 'cal_deleted' ]))
{
$retval = false ;
if ( $this -> debug > 0 ) error_log ( __METHOD__ . " ( $id ) event has been deleted returning " . array2string ( $retval ));
return $retval ;
}
}
2011-04-06 21:26:10 +02:00
2016-05-01 19:47:59 +02:00
if ( ! ( $retval = $this -> bo -> check_perms ( calendar_bo :: ACL_FREEBUSY , $event , 0 , 'server' )) &&
2015-10-09 19:37:01 +02:00
// above can be true, if current user is not in master but just a recurrence
2023-07-24 17:08:05 +02:00
( ! $event [ 'recur_type' ] || ! ( $events = self :: get_series ( $event [ 'uid' ], $this -> bo , false , null , null , $date_format ))))
2010-10-26 11:35:44 +02:00
{
if ( $this -> debug > 0 ) error_log ( __METHOD__ . " ( $id ) no READ or FREEBUSY rights returning " . array2string ( $retval ));
return $retval ;
}
2016-05-01 19:47:59 +02:00
if ( ! $this -> bo -> check_perms ( Acl :: READ , $event , 0 , 'server' ))
2010-05-14 10:35:16 +02:00
{
$this -> bo -> clear_private_infos ( $event , array ( $this -> bo -> user , $event [ 'owner' ]));
}
2011-06-19 10:54:06 +02:00
// handle deleted events, as not existing
if ( $event [ 'deleted' ]) $event = null ;
2010-10-26 11:35:44 +02:00
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " ( $id ) returning " . array2string ( $event ));
2010-05-14 10:35:16 +02:00
return $event ;
2008-05-08 22:31:32 +02:00
}
2013-09-23 12:21:31 +02:00
/**
* Update etag , ctag and sync - token to reflect changed attachments
*
* @ param array | string | int $entry array with entry data from read , or id
*/
public function update_tags ( $entry )
{
if ( ! is_array ( $entry )) $entry = $this -> read ( $entry );
$this -> bo -> update ( $entry , true );
}
2010-04-13 17:31:59 +02:00
/**
* Query ctag for calendar
*
* @ return string
*/
public function getctag ( $path , $user )
{
2010-12-02 23:27:32 +01:00
$ctag = $this -> bo -> get_ctag ( $user , $path == '/calendar/' ? 'owner' : 'default' ); // default = not rejected
2010-04-13 17:31:59 +02:00
2010-06-29 15:52:56 +02:00
if ( $this -> debug > 1 ) error_log ( __FILE__ . '[' . __LINE__ . '] ' . __METHOD__ . " ( $path )[ $user ] = $ctag " );
2010-10-26 11:35:44 +02:00
2011-10-08 13:34:55 +02:00
return $ctag ;
2010-04-13 17:31:59 +02:00
}
2008-05-08 22:31:32 +02:00
/**
2011-10-20 22:10:04 +02:00
* Get the etag for an entry
2008-05-08 22:31:32 +02:00
*
2015-06-25 22:39:53 +02:00
* @ param array | int $entry array with event or cal_id
* @ param string $schedule_tag = null on return schedule - tag
2011-10-20 22:10:04 +02:00
* @ return string | boolean string with etag or false
2008-05-08 22:31:32 +02:00
*/
2011-10-20 22:10:04 +02:00
function get_etag ( $entry , & $schedule_tag = null )
2008-05-08 22:31:32 +02:00
{
2011-10-20 22:10:04 +02:00
$etag = $this -> bo -> get_etag ( $entry , $schedule_tag , $this -> client_shared_uid_exceptions );
2009-12-27 05:21:33 +01:00
//error_log(__METHOD__ . "($entry[id] ($entry[etag]): $entry[title] --> etag=$etag");
2011-10-08 13:34:55 +02:00
return $etag ;
2008-05-08 22:31:32 +02:00
}
2012-10-29 13:23:17 +01:00
/**
* Send response - headers for a PUT ( or POST with add - member query parameter )
*
* Reimplemented to send
*
* @ param int | array $entry id or array of new created entry
* @ param string $path
* @ param int | string $retval
2015-06-25 22:39:53 +02:00
* @ param boolean $path_attr_is_name = true true : path_attr is ca ( l | rd ) dav_name , false : id ( GroupDAV needs Location header )
2021-03-28 20:48:55 +02:00
* @ param string $etag = null etag , to not calculate it again ( if != null )
2021-09-20 18:50:51 +02:00
* @ param string $prefix = ''
2012-10-29 13:23:17 +01:00
*/
2021-09-20 18:50:51 +02:00
function put_response_headers ( $entry , $path , $retval , $path_attr_is_name = true , $etag = null , $prefix = '' )
2012-10-29 13:23:17 +01:00
{
2015-06-25 22:39:53 +02:00
$schedule_tag = null ;
2021-03-28 20:48:55 +02:00
if ( ! isset ( $etag )) $etag = $this -> get_etag ( $entry , $schedule_tag );
2012-10-29 13:23:17 +01:00
if ( $this -> use_schedule_tag )
{
header ( 'Schedule-Tag: "' . $schedule_tag . '"' );
}
2021-09-20 18:50:51 +02:00
parent :: put_response_headers ( $entry , $path , $retval , $path_attr_is_name , $etag , $prefix );
2012-10-29 13:23:17 +01:00
}
2008-05-08 22:31:32 +02:00
/**
* Check if user has the neccessary rights on an event
*
2016-05-01 19:47:59 +02:00
* @ param int $acl Acl :: READ , Acl :: EDIT or Acl :: DELETE
2011-10-20 22:10:04 +02:00
* @ param array | int $event event - array or id
2008-05-08 22:31:32 +02:00
* @ return boolean null if entry does not exist , false if no access , true if access permitted
*/
function check_access ( $acl , $event )
{
2016-05-01 19:47:59 +02:00
if ( $acl == Acl :: READ )
2010-05-14 10:35:16 +02:00
{
2016-05-01 19:47:59 +02:00
// we need at least calendar_bo::ACL_FREEBUSY to get some information
$acl = calendar_bo :: ACL_FREEBUSY ;
2010-05-14 10:35:16 +02:00
}
2008-05-08 22:31:32 +02:00
return $this -> bo -> check_perms ( $acl , $event , 0 , 'server' );
}
/**
* Add extra properties for calendar collections
*
2016-05-01 19:47:59 +02:00
* @ param array $props regular props by the Api\CalDAV handler
2010-03-07 00:06:43 +01:00
* @ param string $displayname
2015-06-25 22:39:53 +02:00
* @ param string $base_uri = null base url of handler
* @ param int $user = null account_id of owner of current collection
* @ param string $path = null path of the collection
2008-05-08 22:31:32 +02:00
* @ return array
*/
2015-06-25 22:39:53 +02:00
public function extra_properties ( array $props , $displayname , $base_uri = null , $user = null , $path = null )
2008-05-08 22:31:32 +02:00
{
2015-06-25 22:39:53 +02:00
unset ( $base_uri ); // unused, but required by function signature
2012-01-30 01:40:55 +01:00
if ( ! isset ( $props [ 'calendar-description' ]))
{
// default calendar description: can be overwritten via PROPPATCH, in which case it's already set
2016-04-02 12:44:17 +02:00
$props [ 'calendar-description' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'calendar-description' , $displayname );
2012-01-30 01:40:55 +01:00
}
2012-01-31 00:53:06 +01:00
$supported_components = array (
2016-04-02 12:44:17 +02:00
Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'comp' , array ( 'name' => 'VEVENT' )),
2012-01-31 00:53:06 +01:00
);
// outbox supports VFREEBUSY too, it is required from OS X iCal to autocomplete locations
if ( substr ( $path , - 8 ) == '/outbox/' )
{
2016-04-02 12:44:17 +02:00
$supported_components [] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'comp' , array ( 'name' => 'VFREEBUSY' ));
2012-01-31 00:53:06 +01:00
}
2016-04-02 12:44:17 +02:00
$props [ 'supported-calendar-component-set' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV ,
2012-01-31 00:53:06 +01:00
'supported-calendar-component-set' , $supported_components );
2012-09-26 16:30:47 +02:00
// supported reports
2012-09-25 13:54:41 +02:00
$props [ 'supported-report-set' ] = array (
2016-04-02 12:44:17 +02:00
'calendar-query' => Api\CalDAV :: mkprop ( 'supported-report' , array (
Api\CalDAV :: mkprop ( 'report' , array (
Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'calendar-query' , '' ))))),
'calendar-multiget' => Api\CalDAV :: mkprop ( 'supported-report' , array (
Api\CalDAV :: mkprop ( 'report' , array (
Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'calendar-multiget' , '' ))))),
'free-busy-query' => Api\CalDAV :: mkprop ( 'supported-report' , array (
Api\CalDAV :: mkprop ( 'report' , array (
Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'free-busy-query' , '' ))))),
2012-09-25 13:54:41 +02:00
);
2013-10-31 12:29:22 +01:00
// rfc 6578 sync-collection report for everything but outbox
2014-01-07 12:10:51 +01:00
// only if "delete-prevention" is switched on (deleted entries get marked deleted but not actualy deleted
2021-05-18 13:11:14 +02:00
if ( strpos ( $path , '/outbox/' ) === false )
2012-10-02 12:35:12 +02:00
{
2016-04-02 12:44:17 +02:00
$props [ 'supported-report-set' ][ 'sync-collection' ] = Api\CalDAV :: mkprop ( 'supported-report' , array (
Api\CalDAV :: mkprop ( 'report' , array (
Api\CalDAV :: mkprop ( 'sync-collection' , '' )))));
2012-10-02 12:35:12 +02:00
}
2016-04-02 12:44:17 +02:00
$props [ 'supported-calendar-data' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'supported-calendar-data' , array (
Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'calendar-data' , array ( 'content-type' => 'text/calendar' , 'version' => '2.0' )),
Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'calendar-data' , array ( 'content-type' => 'text/x-calendar' , 'version' => '1.0' ))));
2010-03-07 00:06:43 +01:00
2011-10-20 15:35:01 +02:00
// get timezone of calendar
2016-04-02 12:44:17 +02:00
if ( $this -> caldav -> prop_requested ( 'calendar-timezone' ))
2011-10-20 15:35:01 +02:00
{
2016-04-02 12:44:17 +02:00
$props [ 'calendar-timezone' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CALDAV , 'calendar-timezone' ,
2011-10-20 16:01:16 +02:00
calendar_timezones :: user_timezone ( $user ));
2011-10-20 15:35:01 +02:00
}
2008-05-08 22:31:32 +02:00
return $props ;
}
2008-11-03 10:36:20 +01:00
/**
* Get the handler and set the supported fields
*
* @ return calendar_ical
*/
private function _get_handler ()
{
2009-06-08 18:21:14 +02:00
$handler = new calendar_ical ();
2008-11-03 10:36:20 +01:00
$handler -> setSupportedFields ( 'GroupDAV' , $this -> agent );
2013-09-23 12:21:31 +02:00
$handler -> supportedFields [ 'attachments' ] = true ; // enabling attachments
2010-08-15 08:42:05 +02:00
if ( $this -> debug > 1 ) error_log ( " ical Handler called: " . $this -> agent );
2008-11-03 10:36:20 +01:00
return $handler ;
}
2012-02-04 02:03:56 +01:00
/**
* Return calendars / addressbooks shared from other users with the current one
*
* return array account_id => account_lid pairs
*/
function get_shared ()
{
$shared = array ();
2015-06-25 22:39:53 +02:00
$pref = $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'groupdav' ][ 'calendar-home-set' ];
$calendar_home_set = $pref ? explode ( ',' , $pref ) : array ();
2012-02-04 02:03:56 +01:00
// replace symbolic id's with real nummeric id's
foreach ( array (
'G' => $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_primary_group' ],
) as $sym => $id )
{
if (( $key = array_search ( $sym , $calendar_home_set )) !== false )
{
$calendar_home_set [ $key ] = $id ;
}
}
foreach ( ExecMethod ( 'calendar.calendar_bo.list_cals' ) as $entry )
{
$id = $entry [ 'grantor' ];
2012-03-01 14:28:38 +01:00
if ( $id && $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ] != $id && // no current user
2012-02-04 02:03:56 +01:00
( in_array ( 'A' , $calendar_home_set ) || in_array (( string ) $id , $calendar_home_set )) &&
is_numeric ( $id ) && ( $owner = $this -> accounts -> id2name ( $id )))
{
2012-09-27 17:46:08 +02:00
$shared [ $id ] = 'calendar-' . $owner ;
}
}
// shared locations and resources
if ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'resources' ])
{
foreach ( array ( 'locations' , 'resources' ) as $res )
{
if (( $pref = $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'groupdav' ][ 'calendar-home-set-' . $res ]))
{
foreach ( explode ( ',' , $pref ) as $res_id )
{
$is_location = $res == 'locations' ;
2016-04-02 12:44:17 +02:00
$shared [ 'r' . $res_id ] = str_replace ( 's/' , '-' , Api\CalDAV\Principals :: resource2name ( $res_id , $is_location ));
2012-09-27 17:46:08 +02:00
}
}
2012-02-04 02:03:56 +01:00
}
}
return $shared ;
}
2012-02-04 02:24:34 +01:00
/**
* Return appliction specific settings
*
2012-02-14 18:38:45 +01:00
* @ param array $hook_data
* @ return array of array with settings
2012-02-04 02:24:34 +01:00
*/
2012-02-14 18:38:45 +01:00
static function get_settings ( $hook_data )
2012-02-04 02:24:34 +01:00
{
2014-07-23 14:30:39 +02:00
$calendars = array (
'A' => lang ( 'All' ),
'G' => lang ( 'Primary Group' ),
);
if ( ! isset ( $hook_data [ 'setup' ]) && in_array ( $hook_data [ 'type' ], array ( 'user' , 'group' )))
2012-02-04 02:24:34 +01:00
{
2014-07-23 14:30:39 +02:00
$user = $hook_data [ 'account_id' ];
foreach ( calendar_bo :: list_calendars ( $user ) as $entry )
2012-02-04 02:24:34 +01:00
{
$calendars [ $entry [ 'grantor' ]] = $entry [ 'name' ];
}
2014-07-23 15:16:01 +02:00
if ( $user > 0 ) unset ( $calendars [ $user ]); // skip current user
2012-02-04 02:24:34 +01:00
}
$settings = array ();
$settings [ 'calendar-home-set' ] = array (
'type' => 'multiselect' ,
'label' => 'Calendars to sync in addition to personal calendar' ,
'name' => 'calendar-home-set' ,
'help' => lang ( 'Only supported by a few fully conformant clients (eg. from Apple). If you have to enter a URL, it will most likly not be suppored!' ) . '<br/>' . lang ( 'They will be sub-folders in users home (%1 attribute).' , 'CalDAV "calendar-home-set"' ),
'values' => $calendars ,
'xmlrpc' => True ,
'admin' => False ,
);
2013-03-14 18:13:59 +01:00
$settings [ 'calendar-past-limit' ] = array (
2016-07-29 16:00:45 +02:00
'type' => 'integer' ,
2013-03-14 18:13:59 +01:00
'label' => lang ( 'How many days to sync in the past (default %1)' , self :: PAST_LIMIT ),
'name' => 'calendar-past-limit' ,
'help' => 'Clients not explicitly stating a limit get limited to these many days. A too high limit may cause problems with some clients.' ,
'xmlrpc' => True ,
'admin' => False ,
);
$settings [ 'calendar-future-limit' ] = array (
2016-07-29 16:00:45 +02:00
'type' => 'integer' ,
2013-03-14 18:13:59 +01:00
'label' => lang ( 'How many days to sync in the future (default %1)' , self :: FUTURE_LIMIT ),
'name' => 'calendar-future-limit' ,
'help' => 'Clients not explicitly stating a limit get limited to these many days. A too high limit may cause problems with some clients.' ,
'xmlrpc' => True ,
'admin' => False ,
);
2012-09-27 17:46:08 +02:00
// allow to subscribe to resources
2016-04-02 12:44:17 +02:00
if ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'resources' ] && ( $all_resources = Api\CalDAV\Principals :: get_resources ()))
2012-09-27 17:46:08 +02:00
{
$resources = $locations = array ();
foreach ( $all_resources as $resource )
{
2016-04-02 12:44:17 +02:00
if ( Api\CalDAV\Principals :: resource_is_location ( $resource ))
2012-09-27 17:46:08 +02:00
{
$locations [ $resource [ 'res_id' ]] = $resource [ 'name' ];
}
else
{
$resources [ $resource [ 'res_id' ]] = $resource [ 'name' ];
}
}
foreach ( array (
'locations' => $locations ,
'resources' => $resources ,
) as $name => $options )
{
if ( $options )
{
natcasesort ( $options );
$settings [ 'calendar-home-set-' . $name ] = array (
'type' => 'multiselect' ,
'label' => lang ( '%1 to sync' , lang ( $name == 'locations' ? 'Location calendars' : 'Resource calendars' )),
'no_lang' => true ,
'name' => 'calendar-home-set-' . $name ,
'help' => lang ( 'Only supported by a few fully conformant clients (eg. from Apple). If you have to enter a URL, it will most likly not be suppored!' ) . '<br/>' . lang ( 'They will be sub-folders in users home (%1 attribute).' , 'CalDAV "calendar-home-set"' ),
'values' => $options ,
'xmlrpc' => True ,
'admin' => False ,
);
}
}
}
2012-02-04 02:24:34 +01:00
return $settings ;
}
2023-02-13 23:16:26 +01:00
}