diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index 6ab684ec09..5443416ee8 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -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 "
no rights to save the alarm=".print_r($alarm,true)." to event($cal_id)
"; 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 } diff --git a/calendar/inc/class.calendar_uiforms.inc.php b/calendar/inc/class.calendar_uiforms.inc.php index 1665cebf79..bda9655b64 100644 --- a/calendar/inc/class.calendar_uiforms.inc.php +++ b/calendar/inc/class.calendar_uiforms.inc.php @@ -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; } @@ -3539,4 +3591,4 @@ class calendar_uiforms extends calendar_ui $etpl->exec('calendar.calendar_uiforms.notify', $content, $sel_options, $readonlys, $preserve,2); } -} +} \ No newline at end of file diff --git a/calendar/lang/egw_de.lang b/calendar/lang/egw_de.lang index b600811c7b..e2b90d143d 100644 --- a/calendar/lang/egw_de.lang +++ b/calendar/lang/egw_de.lang @@ -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.