* Calendar/Mail: allow every participant to apply changes from extern organizer and warn if sender is not identical to iCal organizer or participant

This commit is contained in:
ralf 2022-07-26 14:13:17 +02:00
parent 9c29863079
commit 6afd07da03
6 changed files with 169 additions and 89 deletions

View File

@ -2223,10 +2223,10 @@ class calendar_boupdate extends calendar_bo
*
* @param array $event
* @param array $old_event
* @param Api\DateTime|int|null $instance_date For recurring events, this is the date we
* are dealing with
* @param Api\DateTime|int|null $instance_date For recurring events, this is the date we are dealing with
* @param boolean $ignore_acl=false true: no acl check
*/
function check_move_alarms(Array &$event, Array $old_event = null, $instance_date = null)
function check_move_alarms(Array &$event, Array $old_event = null, $instance_date = null, $ignore_acl=false)
{
if ($old_event !== null && $event['start'] == $old_event['start']) return;
@ -2254,7 +2254,7 @@ class calendar_boupdate extends calendar_bo
else if ($alarm['time'] !== $time->format('ts') - $alarm['offset'])
{
$alarm['time'] = $time->format('ts') - $alarm['offset'];
$this->save_alarm($event['id'], $alarm);
$this->save_alarm($event['id'], $alarm, true, $ignore_acl);
}
}
}
@ -2265,11 +2265,12 @@ class calendar_boupdate extends calendar_bo
* @param int $cal_id Id of the calendar-entry
* @param array $alarm array with fields: text, owner, enabled, ..
* @param boolean $update_modified =true call update modified, default true
* @param boolean $ignore_acl=false true: no acl check
* @return string id of the alarm, or false on error (eg. no perms)
*/
function save_alarm($cal_id, $alarm, $update_modified=true)
function save_alarm($cal_id, $alarm, $update_modified=true, $ignore_acl=false)
{
if (!$cal_id || !$this->check_perms(Acl::EDIT,$alarm['all'] ? $cal_id : 0,!$alarm['all'] ? $alarm['owner'] : 0))
if (!$cal_id || !$ignore_acl && !$this->check_perms(Acl::EDIT,$alarm['all'] ? $cal_id : 0,!$alarm['all'] ? $alarm['owner'] : 0))
{
//echo "<p>no rights to save the alarm=".print_r($alarm,true)." to event($cal_id)</p>";
return false; // no rights to add the alarm
@ -2283,13 +2284,14 @@ class calendar_boupdate extends calendar_bo
* delete one alarms identified by its id
*
* @param string $id alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
* @param boolean $ignore_acl=false true: no acl check
* @return int number of alarms deleted, false on error (eg. no perms)
*/
function delete_alarm($id)
function delete_alarm($id, $ignore_acl=false)
{
list(,$cal_id) = explode(':',$id);
if (!($alarm = $this->so->read_alarm($id)) || !$cal_id || !$this->check_perms(Acl::EDIT,$alarm['all'] ? $cal_id : 0,!$alarm['all'] ? $alarm['owner'] : 0))
if (!($alarm = $this->so->read_alarm($id)) || !$cal_id || !$ignore_acl && !$this->check_perms(Acl::EDIT,$alarm['all'] ? $cal_id : 0,!$alarm['all'] ? $alarm['owner'] : 0))
{
return false; // no rights to delete the alarm
}

View File

@ -2066,7 +2066,6 @@ class calendar_uiforms extends calendar_ui
}
}
/**
* Remove (shared) lock via ajax, when edit popup get's closed
*
@ -2084,6 +2083,34 @@ class calendar_uiforms extends calendar_ui
}
}
/**
* Get email of participant
*
* @param string $uid
* @return string|null
* @throws Exception
*/
public static function participantEmail($uid)
{
if (is_numeric($uid))
{
$email = Api\Accounts::id2name($uid, 'account_email') ?: null;
}
elseif ($uid[0] === 'e')
{
$email = substr($uid, 1);
}
elseif ($uid[0] === 'c' && ($contact = (new Api\Contacts)->read(substr($uid, 1))))
{
$email = $contact['email'] ?? $contact['email_home'];
}
if (!empty($email) && preg_match('/<([^>]+?)>$/', $email, $matches))
{
$email = $matches[1];
}
return $email;
}
/**
* Display iCal meeting request for EMail app and allow to accept, tentative or reject it or a reply and allow to apply it
*
@ -2114,6 +2141,7 @@ class calendar_uiforms extends calendar_ui
$ical_string = $session_data['attachment'];
$ical_charset = $session_data['charset'];
$ical_method = $session_data['method'];
$ical_sender = $session_data['sender'];
unset($session_data);
}
$ical = new calendar_ical();
@ -2140,6 +2168,18 @@ class calendar_uiforms extends calendar_ui
if (($existing_event = $this->bo->read($event['uid'], $event['recurrence'], false, 'ts', null, true)) && // true = read the exception
!$existing_event['deleted'])
{
// check if mail is from extern organizer
$from_extern_organizer = false;
if (strtolower($ical_method) !== 'reply' &&
($extern_organizer = !empty($ical_sender) ? array_filter($existing_event['participants'], static function($status, $user)
{
calendar_so::split_status($status, $quantity, $role);
return $role === 'CHAIR' && is_string($user) && in_array($user[0], ['e', 'c']);
}, ARRAY_FILTER_USE_BOTH) : []) &&
!($from_extern_organizer = $ical_sender === strtolower($organizer=self::participantEmail(key($extern_organizer)))))
{
$event['sender_warning'] = lang('The sender "%1" is NOT the extern organizer "%2", proceed with caution!', $ical_sender, $organizer);
}
switch(strtolower($ical_method))
{
case 'reply':
@ -2150,7 +2190,11 @@ class calendar_uiforms extends calendar_ui
$event['ical_sender_status'] = current($parts);
$quantity = $role = null;
calendar_so::split_status($event['ical_sender_status'], $quantity, $role);
// let user know, that sender is not the participant
if ($ical_sender !== strtolower($participant=self::participantEmail($event['ical_sender_uid'])))
{
$event['sender_warning'] = lang('The sender "%1" is NOT the participant replying "%2", proceed with caution!', $ical_sender, $participant);
}
if ($event['ical_sender_uid'] && $this->bo->check_status_perms($event['ical_sender_uid'], $existing_event))
{
$existing_status = $existing_event['participants'][$event['ical_sender_uid']];
@ -2169,7 +2213,12 @@ class calendar_uiforms extends calendar_ui
case 'request':
$status = $existing_event['participants'][$user];
calendar_so::split_status($status, $quantity, $role);
if (strtolower($ical_method) == 'response' && isset($existing_event['participants'][$user]) &&
if (!empty($extern_organizer) && self::event_changed($event, $existing_event))
{
$event['error'] = lang('The extern organizer changed the event!',);
$readonlys['button[apply]'] = false;
}
elseif (isset($existing_event['participants'][$user]) &&
$status != 'U' && isset($this->bo->verbose_status[$status]))
{
$event['error'] = lang('You already replied to this invitation with').': '.lang($this->bo->verbose_status[$status]);
@ -2185,7 +2234,7 @@ class calendar_uiforms extends calendar_ui
$event['error'] .= ($event['error'] ? "\n" : '').lang('You are not invited to that event!');
if ($event['id'])
{
$readonlys['button[accept]'] = $readonlys['button[tentativ]'] =
$readonlys['button[accept]'] = $readonlys['button[tentativ]'] = $readonlys['button[apply]'] =
$readonlys['button[reject]'] = $readonlys['button[cancel]'] = true;
}
}
@ -2257,6 +2306,54 @@ class calendar_uiforms extends calendar_ui
// clear notification errors
notifications::errors(true);
$msg = [];
// do we need to update the event itself (user-status is reset to old in event_changed!)
if ($button !== 'delete' && !empty($event['old']) && self::event_changed($event, $event['old']))
{
// check if we are allowed to update the event
if($this->bo->check_perms(Acl::EDIT, $event['old']) || $event['extern_organizer'])
{
if ($event['recurrence'] && !$event['old']['reference'] && ($recur_event = $this->bo->read($event['id'])))
{
// first we need to add the exception to the recurrence master
$recur_event['recur_exception'][] = $event['recurrence'];
// check if we need to move the alarms, because they are next on that exception
$this->bo->check_move_alarms($recur_event, null, $event['recurrence'], !empty($event['extern_organizer']));
unset($recur_event['start']); unset($recur_event['end']); // no update necessary
unset($recur_event['alarm']); // unsetting alarms too, as they cant be updated without start!
$this->bo->update($recur_event, $ignore_conflicts=true, true, !empty($event['extern_organizer']), true, $msg, true);
// then we need to create the exception as new event
unset($event['id']);
$event['reference'] = $event['old']['id'];
$event['caldav_name'] = $event['old']['caldav_name'];
}
else
{
// keep all EGroupware only values of existing events plus alarms
unset($event['alarm'], $event['organizer']);
$event = array_merge($event['old'], $event);
}
unset($event['old']);
if (($event['id'] = $this->bo->update($event, $ignore_conflicts=true, true, !empty($event['extern_organizer']), true, $msg, true)))
{
$msg[] = lang('Changed event-data applied');
}
else
{
$msg[] = lang('Error saving the event!');
$button = false;
}
}
else
{
$event['id'] = $event['old']['id'];
// disable "warning" that we have no rights to store any modifications
// as that confuses our users, who only want to accept or reject
//$msg[] = lang('Not enough rights to update the event!');
}
}
switch($button)
{
case 'reject':
@ -2291,53 +2388,6 @@ class calendar_uiforms extends calendar_ui
break;
}
}
// do we need to update the event itself (user-status is reset to old in event_changed!)
elseif (self::event_changed($event, $event['old']))
{
// check if we are allowed to update the event
if($this->bo->check_perms(Acl::EDIT, $event['old']))
{
if ($event['recurrence'] && !$event['old']['reference'] && ($recur_event = $this->bo->read($event['id'])))
{
// first we need to add the exception to the recurrence master
$recur_event['recur_exception'][] = $event['recurrence'];
// check if we need to move the alarms, because they are next on that exception
$this->bo->check_move_alarms($recur_event, null, $event['recurrence']);
unset($recur_event['start']); unset($recur_event['end']); // no update necessary
unset($recur_event['alarm']); // unsetting alarms too, as they cant be updated without start!
$this->bo->update($recur_event, $ignore_conflicts=true, true, false, true, $msg, true);
// then we need to create the exception as new event
unset($event['id']);
$event['reference'] = $event['old']['id'];
$event['caldav_name'] = $event['old']['caldav_name'];
}
else
{
// keep all EGroupware only values of existing events plus alarms
unset($event['alarm']);
$event = array_merge($event['old'], $event);
}
unset($event['old']);
if (($event['id'] = $this->bo->update($event, $ignore_conflicts=true, true, false, true, $msg, true)))
{
$msg[] = lang('Event saved');
}
else
{
$msg[] = lang('Error saving the event!');
break;
}
}
else
{
$event['id'] = $event['old']['id'];
// disable "warning" that we have no rights to store any modifications
// as that confuses our users, who only want to accept or reject
//$msg[] = lang('Not enough rights to update the event!');
}
}
else
{
$event['id'] = $event['old']['id'];
@ -2351,9 +2401,9 @@ class calendar_uiforms extends calendar_ui
case 'apply':
// set status and send notification / meeting response
if ($this->bo->set_status($event['id'], $event['ical_sender_uid'], $event['ical_sender_status'], $event['recurrence']))
if (strtolower($event['ics_method']) === 'reply' && $this->bo->set_status($event['id'], $event['ical_sender_uid'], $event['ical_sender_status'], $event['recurrence']))
{
$msg = lang('Status changed');
$msg[] = lang('Status changed');
}
break;
@ -2361,7 +2411,7 @@ class calendar_uiforms extends calendar_ui
if ($event['id'] && $this->bo->set_status($event['id'], $user, 'R', $event['recurrence'],
false, true, true)) // no reply to organizer
{
$msg = lang('Status changed');
$msg[] = lang('Status changed');
}
break;
@ -2369,7 +2419,7 @@ class calendar_uiforms extends calendar_ui
if ($event['id'] && $this->bo->delete($event['id'], $event['recurrence'],
false, [$event['ical_sender_uid']])) // no reply to organizer
{
$msg = lang('Event deleted.');
$msg[] = lang('Event deleted.');
}
break;
}
@ -2378,7 +2428,7 @@ class calendar_uiforms extends calendar_ui
}
Framework::message(implode("\n", (array)$msg));
$readonlys['button[edit]'] = !$event['id'];
$event['ics_method'] = $readonlys['ics_method'] = strtolower($ical_method);
$event['ics_method'] = strtolower($ical_method);
switch(strtolower($ical_method))
{
case 'reply':
@ -2395,6 +2445,8 @@ class calendar_uiforms extends calendar_ui
$tpl = new Etemplate('calendar.meeting');
$tpl->exec('calendar.calendar_uiforms.meeting', $event, array(), $readonlys, $event+array(
'old' => $existing_event,
'extern_organizer' => $extern_organizer ?? [],
'from_extern_organizer' => $from_extern_organizer ?? false,
), 2);
}
@ -2413,8 +2465,8 @@ class calendar_uiforms extends calendar_ui
'recur_type', 'recur_data', 'recur_interval', 'recur_exception');
// only compare certain fields, taking account unset, null or '' values
$event = array_intersect_key($_event+array('recur_exception'=>array()), array_flip($keys_to_check));
$old = array_intersect_key(array_diff($_old, array(null, '')), array_flip($keys_to_check));
$event = array_intersect_key(array_diff($_event, [null, ''])+array('recur_exception'=>array()), array_flip($keys_to_check));
$old = array_intersect_key(array_diff($_old, [null, '']), array_flip($keys_to_check));
// keep the status of existing participants (users)
foreach($old['participants'] as $uid => $status)
@ -2425,7 +2477,7 @@ class calendar_uiforms extends calendar_ui
}
}
$ret = $event != $old;
$ret = (bool)array_diff_assoc($event, $old);
//error_log(__METHOD__."() returning ".array2string($ret)." diff=".array2string(array_udiff_assoc($event, $old, function($a, $b) { return (int)($a != $b); })));
return $ret;
}

View File

@ -29,6 +29,7 @@ add alarm calendar de Alarm zufügen
add appointments via shortened dialog or complete edit window calendar de Termine hinzufügen über verkürzten Dialog oder komplettes Bearbeiten-Fenster
add current view as favorite calendar de Ansicht als Favorit zufügen
add new alarm calendar de Neuen Alarm erstellen
add new event calendar de Einen neuen Termin hinzufügen
add new participants or resource calendar de Neue(n) Teilnehmer oder Ressource auswählen
add timesheet entry calendar de Stundenzettel hinzufügen
added calendar de Neuer Termin
@ -62,6 +63,7 @@ apply the changes calendar de Übernimmt die Änderungen
appointment settings calendar de Einstellungen der Terminverwaltung
as an alternative you can %1download a mysql dump%2 and import it manually into egw_cal_timezones table. calendar de Als Alternative können Sie auch einen %1MySQL Dump herunterladen%2 und diesen von Hand in die Datenbank Tabelle egw_cal_timezones importieren.
at start of the event calendar de am Beginn des Termins
attention calendar de Achtung
automatically purge old events after admin de Bereinigt bzw. löscht alte Termine automatisch nach
available for the first entry inside each day of week or daily table inside the selected range: calendar de Verfügbar für den ersten Eintrag innerhalb eines jeden Tages oder für die Liste innerhalb des ausgewählten Bereichs:
back half a month calendar de einen halben Monat zurück
@ -578,9 +580,12 @@ the apple ical apps use this color to display events from this calendar. calenda
the document can contain placeholder like {{%1}}, to be replaced with the data. calendar de Das Dokument kann Platzhalter wie {{%1}} enthalten, die durch die Daten ersetzt werden sollen.
the document can contain placeholder like {{%3}}, to be replaced with the data (%1full list of placeholder names%2). calendar de Das Dokument kann Platzhalter wie {{%3}} enthalten, die mit den Daten ersetzt werden (%1komplette Liste der Platzhalter%2)
the exceptions are deleted together with the series. calendar de Die Ausnahmen werden mit der Terminserie gelöscht
the extern organizer changed the event! calendar de Der externe Organisator hat den Termin geändert!
the following document-types are supported: calendar de Im Moment werden die folgenden Dokumenttypen unterstützt:
the original series will be terminated today and a new series be created. calendar de Der bestehende Serientermin wird heute beendet und eine neue Terminserie erstellt.
the resource you selected is already overbooked: calendar de Die Ressource, die Sie buchen möchten, ist bereits überbucht
the sender "%1" is not the extern organizer "%2", proceed with caution! calendar de Der Absender "%1" ist nicht der externe Organisator "%2", seien Sie vorsichtig!
the sender "%1" is not the participant replying "%2", proceed with caution! calendar de Der Absender "%1" ist nicht der antwortende Teilnehmer "%2", seien Sie vorsichtig!
this day is shown as first day in the week or month view. calendar de Dieser Tag wird als erster in der Wochen- oder Monatsansicht angezeigt
this defines the end of your dayview. events after this time, are shown below the dayview. calendar de Diese Zeit definiert das Ende des Arbeitstags in der Tagesansicht. Alle späteren Einträge werden darunter dargestellt
this defines the start of your dayview. events before this time, are shown above the dayview.<br>this time is also used as a default starttime for new events. calendar de Diese Zeit definiert den Anfang des Arbeitstags in der Tagesansicht. Alle früheren Einträge werden darüber dargestellt

View File

@ -63,6 +63,7 @@ apply the changes calendar en Apply the changes
appointment settings calendar en Appointment settings
as an alternative you can %1download a mysql dump%2 and import it manually into egw_cal_timezones table. calendar en As an alternative you can %1download a MySQL dump%2 and import it manually into egw_cal_timezones table.
at start of the event calendar en at start of the event
attention calendar en Attention
automatically purge old events after admin en Automatically purge old events after
available for the first entry inside each day of week or daily table inside the selected range: calendar en Available for the first entry inside each day of week or daily table inside the selected range:
back half a month calendar en Back half a month
@ -579,9 +580,12 @@ the apple ical apps use this color to display events from this calendar. calenda
the document can contain placeholder like {{%1}}, to be replaced with the data. calendar en The document can contain placeholder like {{%1}}, to be replaced with the data.
the document can contain placeholder like {{%3}}, to be replaced with the data (%1full list of placeholder names%2). calendar en The document can contain placeholder like {{%3}}, to be replaced with the data (%1full list of placeholder names%2).
the exceptions are deleted together with the series. calendar en The exceptions are deleted together with the series.
the extern organizer changed the event! calendar en The extern organizer changed the event!
the following document-types are supported: calendar en The following document-types are supported:
the original series will be terminated today and a new series be created. calendar en The original series will be terminated today and a new series be created.
the resource you selected is already overbooked: calendar en The resource you selected is already over-booked:
the sender "%1" is not the extern organizer "%2", proceed with caution! calendar en The sender "%1" is NOT the extern organizer "%2", proceed with caution!
the sender "%1" is not the participant replying "%2", proceed with caution! calendar en The sender "%1" is NOT the participant replying "%2", proceed with caution!
this day is shown as first day in the week or month view. calendar en This day is shown as first day in the week or month view.
this defines the end of your dayview. events after this time, are shown below the dayview. calendar en This defines the end of your day view. Events after this time, are shown below the day view.
this defines the start of your dayview. events before this time, are shown above the dayview.<br>this time is also used as a default starttime for new events. calendar en This defines the start of your day view. Events before this time, are shown above the day view.<br>This time is also used as a default start time for new events.

View File

@ -7,36 +7,49 @@
<column/>
<column/>
<column/>
<column/>
<column/>
</columns>
<rows>
<row disabled="!@sender_warning">
<grid width="100%" class="meetingRequest" span="all">
<columns>
<column/>
</columns>
<rows>
<row class="th">
<description value="Attention"/>
</row>
<row class="row">
<description id="sender_warning" class="meetingRequestError"/>
</row>
</rows>
</grid>
</row>
<row disabled="!@ics_method=request">
<description value="This mail contains a meeting request" class="meetingRequestMessage"/>
<button label="Accept" id="button[accept]" class="leftPad5"/>
<button label="Tentative" id="button[tentativ]" class="leftPad5"/>
<button label="Reject" id="button[reject]" class="leftPad5"/>
<buttononly statustext="Edit event in calendar" label="Edit" id="button[edit]" onclick="window.open(egw::link('/index.php','menuaction=calendar.calendar_uiforms.edit&amp;cal_id=$cont[id]'),'_blank','dependent=yes,width=750,height=410,scrollbars=yes,status=yes'); return false;" class="leftPad5"/>
<hbox>
<button label="Apply" id="button[apply]" class="leftPad5" hideOnReadonly="true"/>
<button label="Accept" id="button[accept]" class="leftPad5" background_image="true" image="calendar/accepted"/>
<button label="Tentative" id="button[tentativ]" class="leftPad5" background_image="true" image="calendar/tentative"/>
<button label="Reject" id="button[reject]" class="leftPad5" background_image="true" image="calendar/rejected"/>
<button statustext="Edit event in calendar" label="Edit" id="button[edit]" background_image="true" image="edit" hideOnReadonly="true"
onclick="window.open(egw::link('/index.php','menuaction=calendar.calendar_uiforms.edit&amp;cal_id=$cont[id]'),'_blank','dependent=yes,width=750,height=410,scrollbars=yes,status=yes'); return false;" class="leftPad5"/>
</hbox>
<description id="error" class="meetingRequestError" align="right"/>
</row>
<row disabled="!@ics_method=reply">
<description value="This mail contains a reply to a meeting request" class="meetingRequestMessage"/>
<button label="Apply" id="button[apply]" class="leftPad5"/>
<description/>
<description/>
<description/>
<description id="error" class="meetingRequestError" align="right"/>
</row>
<row disabled="!@ics_method=cancel">
<description value="This mail cancels a meeting" class="meetingRequestMessage"/>
<hbox>
<button label="Apply" statustext="Removes the event from my calendar" id="button[cancel]" class="leftPad5"/>
<button label="Delete" statustext="Delete this meeting for all participants" id="button[delete]" class="leftPad5"
onclick="et2_dialog.confirm(widget,'Delete this meeting for all participants','Delete')"/>
<buttononly statustext="Edit event in calendar" label="Edit" id="button[edit]"
<button statustext="Edit event in calendar" label="Edit" id="button[edit]" background_image="true" image="edit"
onclick="window.open(egw::link('/index.php','menuaction=calendar.calendar_uiforms.edit&amp;cal_id=$cont[id]'),'_blank','dependent=yes,width=750,height=410,scrollbars=yes,status=yes'); return false;" class="leftPad5"/>
<description/>
<description/>
</hbox>
<description id="error" class="meetingRequestError" align="right"/>
</row>
</rows>

View File

@ -3323,15 +3323,19 @@ $filter['before']= date("d-M-Y", $cutoffdate2);
$bodyParts = $this->mail_bo->getMessageBody($uid, ($htmlOptions?$htmlOptions:''), $partID, $structure, false, $mailbox, $calendar_part);
// for meeting requests (multipart alternative with text/calendar part) let calendar render it
if ($calendar_part && isset($GLOBALS['egw_info']['user']['apps']['calendar']))
if ($calendar_part && isset($GLOBALS['egw_info']['user']['apps']['calendar']) && empty($smime))
{
$charset = $calendar_part->getContentTypeParameter('charset');
// Do not try to fetch raw part content if it's smime signed message
if (!$smime) $this->mail_bo->fetchPartContents($uid, $calendar_part);
$this->mail_bo->fetchPartContents($uid, $calendar_part);
$headers = $this->mail_bo->getHeaders($mailbox, 0, 1, '', false, null, $uid);
Api\Cache::setSession('calendar', 'ical', array(
'charset' => $charset ? $charset : 'utf-8',
'charset' => $charset ?: 'utf-8',
'attachment' => $calendar_part->getContents(),
'method' => $calendar_part->getContentTypeParameter('method'),
'sender' => empty($headers['header'][0]['sender_address']) ? null :
(preg_match('/<([^>]+?)>$/', $sender = strtolower($headers['header'][0]['sender_address']), $matches) ?
$matches[1] : $sender),
));
$this->mail_bo->htmlOptions = $bufferHtmlOptions;
Api\Translation::add_app('calendar');