{"id":1035,"date":"2026-05-05T16:04:29","date_gmt":"2026-05-05T09:04:29","guid":{"rendered":"https:\/\/gotravindo.com\/?page_id=1035"},"modified":"2026-05-06T11:16:42","modified_gmt":"2026-05-06T04:16:42","slug":"staff","status":"publish","type":"page","link":"https:\/\/gotravindo.com\/id\/staff\/","title":{"rendered":"staff"},"content":{"rendered":"<div class=\"wpb-content-wrapper\"><div class=\"vc_row wpb_row vc_row-fluid\"><div class=\"wpb_column vc_column_container vc_col-sm-12\"><div class=\"vc_column-inner\"><div class=\"wpb_wrapper\">\n\t<div class=\"wpb_raw_code wpb_raw_html wpb_content_element\" >\n\t\t<div class=\"wpb_wrapper\">\n\t\t\t<?php\r\n\r\n\/**\r\n * Calendar plugin for Roundcube webmail\r\n *\r\n * @author Lazlo Westerhof <hello@lazlo.me>\r\n * @author Thomas Bruederli <bruederli@kolabsys.com>\r\n *\r\n * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>\r\n * Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>\r\n *\r\n * This program is free software: you can redistribute it and\/or modify\r\n * it under the terms of the GNU Affero General Public License as\r\n * published by the Free Software Foundation, either version 3 of the\r\n * License, or (at your option) any later version.\r\n *\r\n * This program is distributed in the hope that it will be useful,\r\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n * GNU Affero General Public License for more details.\r\n *\r\n * You should have received a copy of the GNU Affero General Public License\r\n * along with this program. If not, see <http:>.\r\n *\/\r\n\r\n\/**\r\n * Calendar plugin\r\n *\r\n * @property calendar_driver          $driver\r\n * @property libcalendaring_vcalendar $ical\r\n * @property libcalendaring_itip      $itip\r\n *\/\r\n#[AllowDynamicProperties]\r\nclass calendar extends rcube_plugin\r\n{\r\n    public const FREEBUSY_UNKNOWN   = 0;\r\n    public const FREEBUSY_FREE      = 1;\r\n    public const FREEBUSY_BUSY      = 2;\r\n    public const FREEBUSY_TENTATIVE = 3;\r\n    public const FREEBUSY_OOF       = 4;\r\n\r\n    public const SESSION_KEY = 'calendar_temp';\r\n\r\n    public $task = '?(?!logout).*';\r\n\r\n    \/** @var rcmail *\/\r\n    public $rc;\r\n    public $lib;\r\n    public $resources_dir;\r\n    public $home;  \/\/ declare public to be used in other classes\r\n    public $urlbase;\r\n    public $timezone;\r\n    public $timezone_offset;\r\n    public $gmt_offset;\r\n    public $dst_active;\r\n    public $ui;\r\n    public $event;\r\n    public $invitestatus;\r\n\r\n    public $defaults = [\r\n        'calendar_default_view' => \"agendaWeek\",\r\n        'calendar_timeslots'    => 2,\r\n        'calendar_work_start'   => 6,\r\n        'calendar_work_end'     => 18,\r\n        'calendar_agenda_range' => 60,\r\n        'calendar_show_weekno'  => 0,\r\n        'calendar_first_day'    => 1,\r\n        'calendar_first_hour'   => 6,\r\n        'calendar_time_format'  => null,\r\n        'calendar_event_coloring'      => 0,\r\n        'calendar_time_indicator'      => true,\r\n        'calendar_allow_invite_shared' => false,\r\n        'calendar_itip_send_option'    => 3,\r\n        'calendar_itip_after_action'   => 0,\r\n    ];\r\n\r\n    private $token;\r\n\r\n\r\n    \/**\r\n     * Plugin initialization.\r\n     *\/\r\n    public function init()\r\n    {\r\n        $this->rc = rcmail::get_instance();\r\n\r\n        $this->register_task('calendar');\r\n\r\n        \/\/ load calendar configuration\r\n        $this->load_config();\r\n\r\n        \/\/ catch iTIP confirmation requests that don're require a valid session\r\n        if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) {\r\n            $this->add_hook('startup', [$this, 'itip_attend_response']);\r\n        } elseif ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) {\r\n            $this->add_hook('startup', [$this, 'ical_feed_export']);\r\n        } elseif ($this->rc->task != 'login') {\r\n            \/\/ default startup routine\r\n            $this->add_hook('startup', [$this, 'startup']);\r\n        }\r\n\r\n        $this->add_hook('user_delete', [$this, 'user_delete']);\r\n    }\r\n\r\n    \/**\r\n     * Setup basic plugin environment and UI\r\n     *\/\r\n    protected function setup()\r\n    {\r\n        $this->require_plugin('libcalendaring');\r\n        $this->require_plugin('libkolab');\r\n\r\n        require $this->home . '\/lib\/calendar_ui.php';\r\n\r\n        \/\/ load localizations\r\n        $this->add_texts('localization\/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print'));\r\n\r\n        $this->lib             = libcalendaring::get_instance();\r\n        $this->timezone        = $this->lib->timezone;\r\n        $this->gmt_offset      = $this->lib->gmt_offset;\r\n        $this->dst_active      = $this->lib->dst_active;\r\n        $this->timezone_offset = $this->gmt_offset \/ 3600 - $this->dst_active;\r\n        $this->ui              = new calendar_ui($this);\r\n    }\r\n\r\n    \/**\r\n     * Startup hook\r\n     *\/\r\n    public function startup($args)\r\n    {\r\n        \/\/ the calendar module can be enabled\/disabled by the kolab_auth plugin\r\n        if ($this->rc->config->get('calendar_disabled', false)\r\n            || !$this->rc->config->get('calendar_enabled', true)\r\n        ) {\r\n            return;\r\n        }\r\n\r\n        $this->setup();\r\n\r\n        \/\/ load Calendar user interface\r\n        if (!$this->rc->output->ajax_call\r\n            && (empty($this->rc->output->env['framed']) || $args['action'] == 'preview')\r\n        ) {\r\n            $this->ui->init();\r\n\r\n            \/\/ settings are required in (almost) every GUI step\r\n            if ($args['action'] != 'attend') {\r\n                $this->rc->output->set_env('calendar_settings', $this->load_settings());\r\n            }\r\n\r\n            \/\/ A hack to replace \"Edit\/Share Calendar\" label with \"Edit calendar\", for non-Kolab driver\r\n            if ($args['task'] == 'calendar' && $this->rc->config->get('calendar_driver', 'database') !== 'kolab') {\r\n                $merge = ['calendar.editcalendar' => $this->gettext('edcalendar')];\r\n                $this->rc->load_language(null, [], $merge);\r\n                $this->rc->output->command('add_label', $merge);\r\n            }\r\n        }\r\n\r\n        if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') {\r\n            if ($args['action'] != 'upload') {\r\n                $this->load_driver();\r\n            }\r\n\r\n            \/\/ register calendar actions\r\n            $this->register_action('index', [$this, 'calendar_view']);\r\n            $this->register_action('event', [$this, 'event_action']);\r\n            $this->register_action('calendar', [$this, 'calendar_action']);\r\n            $this->register_action('count', [$this, 'count_events']);\r\n            $this->register_action('load_events', [$this, 'load_events']);\r\n            $this->register_action('export_events', [$this, 'export_events']);\r\n            $this->register_action('import_events', [$this, 'import_events']);\r\n            $this->register_action('upload', [$this, 'attachment_upload']);\r\n            $this->register_action('get-attachment', [$this, 'attachment_get']);\r\n            $this->register_action('freebusy-status', [$this, 'freebusy_status']);\r\n            $this->register_action('freebusy-times', [$this, 'freebusy_times']);\r\n            $this->register_action('randomdata', [$this, 'generate_randomdata']);\r\n            $this->register_action('print', [$this,'print_view']);\r\n            $this->register_action('mailimportitip', [$this, 'mail_import_itip']);\r\n            $this->register_action('mailimportattach', [$this, 'mail_import_attachment']);\r\n            $this->register_action('dialog-ui', [$this, 'mail_message2event']);\r\n            $this->register_action('check-recent', [$this, 'check_recent']);\r\n            $this->register_action('itip-status', [$this, 'event_itip_status']);\r\n            $this->register_action('itip-remove', [$this, 'event_itip_remove']);\r\n            $this->register_action('itip-decline-reply', [$this, 'mail_itip_decline_reply']);\r\n            $this->register_action('itip-delegate', [$this, 'mail_itip_delegate']);\r\n            $this->register_action('resources-list', [$this, 'resources_list']);\r\n            $this->register_action('resources-owner', [$this, 'resources_owner']);\r\n            $this->register_action('resources-calendar', [$this, 'resources_calendar']);\r\n            $this->register_action('resources-autocomplete', [$this, 'resources_autocomplete']);\r\n            $this->register_action('talk-room-create', [$this, 'talk_room_create']);\r\n\r\n            $this->rc->plugins->register_action('plugin.share-invitation', $this->ID, [$this, 'share_invitation']);\r\n\r\n            $this->add_hook('refresh', [$this, 'refresh']);\r\n\r\n            \/\/ remove undo information...\r\n            if (!empty($_SESSION['calendar_event_undo'])) {\r\n                $undo = $_SESSION['calendar_event_undo'];\r\n                \/\/ ...after timeout\r\n                $undo_time = $this->rc->config->get('undo_timeout', 0);\r\n                if ($undo['ts'] < time() - $undo_time) {\r\n                    $this->rc->session->remove('calendar_event_undo');\r\n                    \/\/ @TODO: do EXPUNGE on kolab objects?\r\n                }\r\n            }\r\n        } elseif ($args['task'] == 'settings') {\r\n            \/\/ add hooks for Calendar settings\r\n            $this->add_hook('preferences_sections_list', [$this, 'preferences_sections_list']);\r\n            $this->add_hook('preferences_list', [$this, 'preferences_list']);\r\n            $this->add_hook('preferences_save', [$this, 'preferences_save']);\r\n        } elseif ($args['task'] == 'mail') {\r\n            \/\/ hooks to catch event invitations on incoming mails\r\n            if ($args['action'] == 'show' || $args['action'] == 'preview') {\r\n                $this->add_hook('template_object_messagebody', [$this, 'mail_messagebody_html']);\r\n            }\r\n\r\n            \/\/ add 'Create event' item to message menu\r\n            if ($this->api->output->type == 'html' && (empty($_GET['_rel']) || $_GET['_rel'] != 'event')) {\r\n                $this->api->output->add_label('calendar.createfrommail');\r\n                $this->api->add_content(\r\n                    html::tag(\r\n                        'li',\r\n                        ['role' => 'menuitem'],\r\n                        $this->api->output->button([\r\n                            'command'  => 'calendar-create-from-mail',\r\n                            'label'    => 'calendar.createfrommail',\r\n                            'type'     => 'link',\r\n                            'classact' => 'icon calendarlink active',\r\n                            'class'    => 'icon calendarlink disabled',\r\n                            'innerclass' => 'icon calendar',\r\n                        ])\r\n                    ),\r\n                    'messagemenu'\r\n                );\r\n            }\r\n\r\n            $this->add_hook('messages_list', [$this, 'mail_messages_list']);\r\n            $this->add_hook('message_compose', [$this, 'mail_message_compose']);\r\n        } elseif ($args['task'] == 'addressbook') {\r\n            if ($this->rc->config->get('calendar_contact_birthdays')) {\r\n                $this->add_hook('contact_update', [$this, 'contact_update']);\r\n                $this->add_hook('contact_create', [$this, 'contact_update']);\r\n            }\r\n        }\r\n\r\n        \/\/ add hooks to display alarms\r\n        $this->add_hook('pending_alarms', [$this, 'pending_alarms']);\r\n        $this->add_hook('dismiss_alarms', [$this, 'dismiss_alarms']);\r\n    }\r\n\r\n    \/**\r\n     * Helper method to load the backend driver according to local config\r\n     *\/\r\n    private function load_driver()\r\n    {\r\n        if (!empty($this->driver)) {\r\n            return;\r\n        }\r\n\r\n        $driver_name = $this->rc->config->get('calendar_driver', 'database');\r\n        $driver_class = $driver_name . '_driver';\r\n\r\n        require_once $this->home . '\/drivers\/calendar_driver.php';\r\n        require_once $this->home . '\/drivers\/' . $driver_name . '\/' . $driver_class . '.php';\r\n\r\n        $this->driver = new $driver_class($this);\r\n\r\n        if ($this->driver->undelete) {\r\n            $this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0;\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Load iTIP functions\r\n     *\/\r\n    private function load_itip()\r\n    {\r\n        if (empty($this->itip)) {\r\n            require_once $this->home . '\/lib\/calendar_itip.php';\r\n\r\n            $this->itip = new calendar_itip($this);\r\n\r\n            $rsvp_actions = ['accepted','tentative','declined','delegated'];\r\n\r\n            if ($this->rc->config->get('kolab_invitation_calendars')) {\r\n                $rsvp_actions[] = 'needs-action';\r\n            }\r\n\r\n            $this->itip->set_rsvp_actions($this->rc->config->get('calendar_rsvp_actions', $rsvp_actions));\r\n        }\r\n\r\n        return $this->itip;\r\n    }\r\n\r\n    \/**\r\n     * Load iCalendar functions\r\n     *\/\r\n    public function get_ical()\r\n    {\r\n        if (empty($this->ical)) {\r\n            $this->ical = libcalendaring::get_ical();\r\n        }\r\n\r\n        return $this->ical;\r\n    }\r\n\r\n    \/**\r\n     * Get properties of the calendar this user has specified as default\r\n     *\/\r\n    public function get_default_calendar($calendars = null)\r\n    {\r\n        if ($calendars === null) {\r\n            $filter    = calendar_driver::FILTER_WRITEABLE;\r\n            $calendars = $this->driver->list_calendars($filter);\r\n        }\r\n\r\n        $default_id = $this->rc->config->get('calendar_default_calendar');\r\n        $calendar   = !empty($calendars[$default_id]) ? $calendars[$default_id] : null;\r\n        $first      = null;\r\n\r\n        if (!$calendar) {\r\n            foreach ($calendars as $cal) {\r\n                if (!empty($cal['default']) && $cal['editable']) {\r\n                    $calendar = $cal;\r\n                }\r\n                if ($cal['editable']) {\r\n                    $first = $cal;\r\n                }\r\n            }\r\n        }\r\n\r\n        return $calendar ?: $first;\r\n    }\r\n\r\n    \/**\r\n     * Render the main calendar view from skin template\r\n     *\/\r\n    public function calendar_view()\r\n    {\r\n        $this->rc->output->set_pagetitle($this->gettext('calendar'));\r\n\r\n        \/\/ Add JS files to the page header\r\n        $this->ui->addJS();\r\n\r\n        $this->ui->init_templates();\r\n        $this->rc->output->add_label(\r\n            'lowest',\r\n            'low',\r\n            'normal',\r\n            'high',\r\n            'highest',\r\n            'delete',\r\n            'cancel',\r\n            'uploading',\r\n            'noemailwarning',\r\n            'close'\r\n        );\r\n\r\n        \/\/ initialize attendees autocompletion\r\n        $this->rc->autocomplete_init();\r\n\r\n        $this->rc->output->set_env('timezone', $this->timezone->getName());\r\n        $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);\r\n        $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver'));\r\n        $this->rc->output->set_env('calendar_resources_freebusy', !empty($this->rc->config->get('kolab_freebusy_server')));\r\n        $this->rc->output->set_env('identities-selector', $this->ui->identity_select([\r\n                'id'         => 'edit-identities-list',\r\n                'aria-label' => $this->gettext('roleorganizer'),\r\n                'class'      => 'form-control custom-select',\r\n        ]));\r\n\r\n        $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC);\r\n        if (in_array($view, ['agendaWeek', 'agendaDay', 'month', 'list'])) {\r\n            $this->rc->output->set_env('view', $view);\r\n        }\r\n\r\n        if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC)) {\r\n            $this->rc->output->set_env('date', $date);\r\n        }\r\n\r\n        if ($msgref = rcube_utils::get_input_value('itip', rcube_utils::INPUT_GPC)) {\r\n            $this->rc->output->set_env('itip_events', $this->itip_events($msgref));\r\n        }\r\n\r\n        $this->rc->output->send('calendar.calendar');\r\n    }\r\n\r\n    \/**\r\n     * Handler for preferences_sections_list hook.\r\n     * Adds Calendar settings sections into preferences sections list.\r\n     *\r\n     * @param array $p Original parameters\r\n     *\r\n     * @return array Modified parameters\r\n     *\/\r\n    public function preferences_sections_list($p)\r\n    {\r\n        $p['list']['calendar'] = [\r\n            'id'      => 'calendar',\r\n            'section' => $this->gettext('calendar'),\r\n        ];\r\n\r\n        return $p;\r\n    }\r\n\r\n    \/**\r\n     * Handler for preferences_list hook.\r\n     * Adds options blocks into Calendar settings sections in Preferences.\r\n     *\r\n     * @param array $p Original parameters\r\n     *\r\n     * @return array Modified parameters\r\n     *\/\r\n    public function preferences_list($p)\r\n    {\r\n        if ($p['section'] != 'calendar') {\r\n            return $p;\r\n        }\r\n\r\n        $no_override = array_flip((array) $this->rc->config->get('dont_override'));\r\n\r\n        $p['blocks']['view']['name'] = $this->gettext('mainoptions');\r\n\r\n        if (!isset($no_override['calendar_default_view'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['view']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $field_id = 'rcmfd_default_view';\r\n            $view = $this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);\r\n\r\n            $select = new html_select(['name' => '_default_view', 'id' => $field_id]);\r\n            $select->add($this->gettext('day'), \"agendaDay\");\r\n            $select->add($this->gettext('week'), \"agendaWeek\");\r\n            $select->add($this->gettext('month'), \"month\");\r\n            $select->add($this->gettext('agenda'), \"list\");\r\n\r\n            $p['blocks']['view']['options']['default_view'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('default_view'))),\r\n                'content' => $select->show($view == 'table' ? 'list' : $view),\r\n            ];\r\n        }\r\n\r\n        if (!isset($no_override['calendar_timeslots'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['view']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $field_id  = 'rcmfd_timeslots';\r\n            $choices   = ['1', '2', '3', '4', '6'];\r\n            $timeslots = $this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);\r\n\r\n            $select = new html_select(['name' => '_timeslots', 'id' => $field_id]);\r\n            $select->add($choices, $choices);\r\n\r\n            $p['blocks']['view']['options']['timeslots'] = [\r\n                'title' => html::label($field_id, rcube::Q($this->gettext('timeslots'))),\r\n                'content' => $select->show(strval($timeslots)),\r\n            ];\r\n        }\r\n\r\n        if (!isset($no_override['calendar_first_day'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['view']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $field_id  = 'rcmfd_firstday';\r\n            $first_day = $this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);\r\n\r\n            $select = new html_select(['name' => '_first_day', 'id' => $field_id]);\r\n            $select->add($this->gettext('sunday'), '0');\r\n            $select->add($this->gettext('monday'), '1');\r\n            $select->add($this->gettext('tuesday'), '2');\r\n            $select->add($this->gettext('wednesday'), '3');\r\n            $select->add($this->gettext('thursday'), '4');\r\n            $select->add($this->gettext('friday'), '5');\r\n            $select->add($this->gettext('saturday'), '6');\r\n\r\n            $p['blocks']['view']['options']['first_day'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('first_day'))),\r\n                'content' => $select->show(strval($first_day)),\r\n            ];\r\n        }\r\n\r\n        if (!isset($no_override['calendar_first_hour'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['view']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $time_format = $this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);\r\n            $time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($time_format));\r\n            $first_hour  = $this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);\r\n            $field_id    = 'rcmfd_firsthour';\r\n\r\n            $select_hours = new html_select(['name' => '_first_hour', 'id' => $field_id]);\r\n            for ($h = 0; $h < 24; $h++) {\r\n                $select_hours->add(date($time_format, mktime($h, 0, 0)), $h);\r\n            }\r\n\r\n            $p['blocks']['view']['options']['first_hour'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('first_hour'))),\r\n                'content' => $select_hours->show($first_hour),\r\n            ];\r\n        }\r\n\r\n        if (!isset($no_override['calendar_work_start'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['view']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $field_id   = 'rcmfd_workstart';\r\n            $work_start = $this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);\r\n            $work_end   = $this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);\r\n            $time_format = $this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);\r\n            $time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($time_format));\r\n\r\n            $select_hours = new html_select(['name' => '_work_start', 'id' => $field_id]);\r\n            for ($h = 0; $h < 24; $h++) {\r\n                $select_hours->add(date($time_format, mktime($h, 0, 0)), $h);\r\n            }\r\n\r\n            $p['blocks']['view']['options']['workinghours'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('workinghours'))),\r\n                'content' => html::div(\r\n                    'input-group',\r\n                    $select_hours->show($work_start)\r\n                    . html::span('input-group-append input-group-prepend', html::span('input-group-text', ' &mdash; '))\r\n                    . $select_hours->show($work_end, ['name' => '_work_end', 'id' => $field_id])\r\n                ),\r\n            ];\r\n        }\r\n\r\n        if (!isset($no_override['calendar_event_coloring'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['view']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $field_id = 'rcmfd_coloring';\r\n            $mode     = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);\r\n\r\n            $select_colors = new html_select(['name' => '_event_coloring', 'id' => $field_id]);\r\n            $select_colors->add($this->gettext('coloringmode0'), 0);\r\n            $select_colors->add($this->gettext('coloringmode1'), 1);\r\n            $select_colors->add($this->gettext('coloringmode2'), 2);\r\n            $select_colors->add($this->gettext('coloringmode3'), 3);\r\n\r\n            $p['blocks']['view']['options']['eventcolors'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('eventcoloring'))),\r\n                'content' => $select_colors->show($mode),\r\n            ];\r\n        }\r\n\r\n        \/\/ loading driver is expensive, don't do it if not needed\r\n        $this->load_driver();\r\n\r\n        if (!isset($no_override['calendar_default_alarm_type']) || !isset($no_override['calendar_default_alarm_offset'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['view']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $alarm_type = $alarm_offset = '';\r\n            $field_id    = 'rcmfd_alarm';\r\n\r\n            if (!isset($no_override['calendar_default_alarm_type'])) {\r\n                $select_type = new html_select(['name' => '_alarm_type', 'id' => $field_id]);\r\n                $select_type->add($this->gettext('none'), '');\r\n\r\n                foreach ($this->driver->alarm_types as $type) {\r\n                    $select_type->add($this->rc->gettext(strtolower(\"alarm{$type}option\"), 'libcalendaring'), $type);\r\n                }\r\n\r\n                $alarm_type = $select_type->show($this->rc->config->get('calendar_default_alarm_type', ''));\r\n            }\r\n\r\n            if (!isset($no_override['calendar_default_alarm_offset'])) {\r\n                $input_value   = new html_inputfield(['name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3]);\r\n                $select_offset = new html_select(['name' => '_alarm_offset', 'id' => $field_id . 'offset']);\r\n\r\n                foreach (['-M','-H','-D','+M','+H','+D'] as $trigger) {\r\n                    $select_offset->add($this->rc->gettext('trigger' . $trigger, 'libcalendaring'), $trigger);\r\n                }\r\n\r\n                $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));\r\n                $alarm_offset = $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]);\r\n            }\r\n\r\n            $p['blocks']['view']['options']['alarmtype'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('defaultalarmtype'))),\r\n                'content' => html::div('input-group', $alarm_type . ' ' . $alarm_offset),\r\n            ];\r\n        }\r\n\r\n        if (!isset($no_override['calendar_default_calendar'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['view']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            \/\/ default calendar selection\r\n            $field_id   = 'rcmfd_default_calendar';\r\n            $filter     = calendar_driver::FILTER_INSERTABLE;\r\n            $select_cal = new html_select(['name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true]);\r\n\r\n            $default_calendar = null;\r\n            foreach ((array) $this->driver->list_calendars($filter) as $id => $prop) {\r\n                $select_cal->add($prop['name'], strval($id));\r\n                if (!empty($prop['default'])) {\r\n                    $default_calendar = $id;\r\n                }\r\n            }\r\n\r\n            $p['blocks']['view']['options']['defaultcalendar'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('defaultcalendar'))),\r\n                'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)),\r\n            ];\r\n        }\r\n\r\n        if (!isset($no_override['calendar_show_weekno'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['view']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $field_id   = 'rcmfd_show_weekno';\r\n            $select = new html_select(['name' => '_show_weekno', 'id' => $field_id]);\r\n            $select->add($this->gettext('weeknonone'), -1);\r\n            $select->add($this->gettext('weeknodatepicker'), 0);\r\n            $select->add($this->gettext('weeknoall'), 1);\r\n\r\n            $p['blocks']['view']['options']['show_weekno'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('showweekno'))),\r\n                'content' => $select->show(intval($this->rc->config->get('calendar_show_weekno'))),\r\n            ];\r\n        }\r\n\r\n        $p['blocks']['itip']['name'] = $this->gettext('itipoptions');\r\n\r\n        \/\/ Invitations handling\r\n        if (!isset($no_override['calendar_itip_after_action'])) {\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['itip']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $field_id = 'rcmfd_after_action';\r\n            $select   = new html_select([\r\n                    'name'     => '_after_action',\r\n                    'id'       => $field_id,\r\n                    'onchange' => \"\\$('#{$field_id}_select')[this.value == 4 ? 'show' : 'hide']()\",\r\n            ]);\r\n\r\n            $select->add($this->gettext('afternothing'), '');\r\n            $select->add($this->gettext('aftertrash'), 1);\r\n            $select->add($this->gettext('afterdelete'), 2);\r\n            $select->add($this->gettext('afterflagdeleted'), 3);\r\n            $select->add($this->gettext('aftermoveto'), 4);\r\n\r\n            $val    = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);\r\n            $folder = null;\r\n\r\n            if ($val !== null && $val !== '' && !is_int($val)) {\r\n                $folder = $val;\r\n                $val    = 4;\r\n            }\r\n\r\n            $folders = $this->rc->folder_selector([\r\n                    'id'            => $field_id . '_select',\r\n                    'name'          => '_after_action_folder',\r\n                    'maxlength'     => 30,\r\n                    'folder_filter' => 'mail',\r\n                    'folder_rights' => 'w',\r\n                    'style'         => $val !== 4 ? 'display:none' : '',\r\n            ]);\r\n\r\n            $p['blocks']['itip']['options']['after_action'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('afteraction'))),\r\n                'content' => html::div(\r\n                    'input-group input-group-combo',\r\n                    $select->show($val) . $folders->show($folder)\r\n                ),\r\n            ];\r\n        }\r\n\r\n        \/\/ category definitions\r\n        if (empty($this->driver->nocategories) && !isset($no_override['calendar_categories'])) {\r\n            $p['blocks']['categories']['name'] = $this->gettext('categories');\r\n\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['categories']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $categories      = (array) $this->driver->list_categories();\r\n            $categories_list = '';\r\n\r\n            foreach ($categories as $name => $color) {\r\n                $key = md5($name);\r\n                $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);\r\n                $category_remove = html::span(\r\n                    'input-group-append',\r\n                    html::a(\r\n                        [\r\n                            'class'   => 'button icon delete input-group-text',\r\n                            'onclick' => '$(this).parent().parent().remove()',\r\n                            'title'   => $this->gettext('remove_category'),\r\n                            'href'    => '#rcmfd_new_category',\r\n                        ],\r\n                        html::span('inner', $this->gettext('delete'))\r\n                    )\r\n                );\r\n\r\n                $category_name  = new html_inputfield(['name' => \"_categories[$key]\", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable]);\r\n                $category_color = new html_inputfield(['name' => \"_colors[$key]\", 'class' => \"$field_class colors\", 'size' => 6]);\r\n                $hidden         = '';\r\n\r\n                if (!empty($this->driver->categoriesimmutable)) {\r\n                    $hidden =  html::tag('input', ['type' => 'hidden', 'name' => \"_categories[$key]\", 'value' => $name]);\r\n                }\r\n\r\n                $categories_list .= $hidden\r\n                    . html::div('input-group', $category_name->show($name) . $category_color->show($color) . $category_remove);\r\n            }\r\n\r\n            $p['blocks']['categories']['options']['categoriesdefault'] = [\r\n                'content' => html::div(['id' => 'calendarcategories'], $categories_list),\r\n            ];\r\n\r\n            $field_id = 'rcmfd_new_category';\r\n            $new_category = new html_inputfield(['name' => '_new_category', 'id' => $field_id, 'size' => 30]);\r\n            $add_category = html::span(\r\n                'input-group-append',\r\n                html::a(\r\n                    [\r\n                        'type'    => 'button',\r\n                        'class'   => 'button create input-group-text',\r\n                        'title'   => $this->gettext('add_category'),\r\n                        'onclick' => 'rcube_calendar_add_category()',\r\n                        'href'    => '#rcmfd_new_category',\r\n                    ],\r\n                    html::span('inner', $this->gettext('add_category'))\r\n                )\r\n            );\r\n\r\n            $p['blocks']['categories']['options']['categories'] = [\r\n                'content' => html::div('input-group', $new_category->show('') . $add_category),\r\n            ];\r\n\r\n            $this->rc->output->add_label('delete', 'calendar.remove_category');\r\n            $this->rc->output->add_script(\r\n                '\r\nfunction rcube_calendar_add_category() {\r\n    var name = $(\"#rcmfd_new_category\").val();\r\n    if (name.length) {\r\n        var button_label = rcmail.gettext(\"calendar.remove_category\");\r\n        var input = $(\"<input>\").attr({type: \"text\", name: \"_categories[]\", size: 30, \"class\": \"form-control\"}).val(name);\r\n        var color = $(\"<input>\").attr({type: \"text\", name: \"_colors[]\", size: 6, \"class\": \"colors form-control\"}).val(\"000000\");\r\n        var button = $(\"<a>\").attr({\"class\": \"button icon delete input-group-text\", title: button_label, href: \"#rcmfd_new_category\"})\r\n            .click(function() { $(this).parent().parent().remove(); })\r\n            .append($(\"<span>\").addClass(\"inner\").text(rcmail.gettext(\"delete\")));\r\n\r\n        $(\"<div>\").addClass(\"input-group\").append(input).append(color).append($(\"<span class=\"\\'input-group-append\\'\">\").append(button))\r\n            .appendTo(\"#calendarcategories\");\r\n        color.minicolors(rcmail.env.minicolors_config || {});\r\n        $(\"#rcmfd_new_category\").val(\"\");\r\n    }\r\n}',\r\n                'foot'\r\n            );\r\n\r\n            $this->rc->output->add_script(\r\n                '\r\n$(\"#rcmfd_new_category\").keypress(function(event) {\r\n    if (event.which == 13) {\r\n        rcube_calendar_add_category();\r\n        event.preventDefault();\r\n    }\r\n});',\r\n                'docready'\r\n            );\r\n\r\n            \/\/ load miniColors js\/css files\r\n            jqueryui::miniColors();\r\n        }\r\n\r\n        \/\/ virtual birthdays calendar\r\n        if (!isset($no_override['calendar_contact_birthdays'])) {\r\n            $p['blocks']['birthdays']['name'] = $this->gettext('birthdayscalendar');\r\n\r\n            if (empty($p['current'])) {\r\n                $p['blocks']['birthdays']['content'] = true;\r\n                return $p;\r\n            }\r\n\r\n            $field_id = 'rcmfd_contact_birthdays';\r\n            $input    = new html_checkbox([\r\n                    'name'    => '_contact_birthdays',\r\n                    'id'      => $field_id,\r\n                    'value'   => 1,\r\n                    'onclick' => '$(\".calendar_birthday_props\").prop(\"disabled\",!this.checked)',\r\n            ]);\r\n\r\n            $p['blocks']['birthdays']['options']['contact_birthdays'] = [\r\n                'title'   => html::label($field_id, $this->gettext('displaybirthdayscalendar')),\r\n                'content' => $input->show($this->rc->config->get('calendar_contact_birthdays') ? 1 : 0),\r\n            ];\r\n\r\n            $input_attrib = [\r\n                'class'    => 'calendar_birthday_props',\r\n                'disabled' => !$this->rc->config->get('calendar_contact_birthdays'),\r\n            ];\r\n\r\n            $sources  = [];\r\n            $checkbox = new html_checkbox(['name' => '_birthday_adressbooks[]'] + $input_attrib);\r\n\r\n            foreach ($this->rc->get_address_sources(false, true) as $source) {\r\n                \/\/ Roundcube >= 1.5, Ignore Collected Recipients and Trusted Senders sources\r\n                if ((defined('rcube_addressbook::TYPE_RECIPIENT') && $source['id'] == (string) rcube_addressbook::TYPE_RECIPIENT)\r\n                    || (defined('rcube_addressbook::TYPE_TRUSTED_SENDER') && $source['id'] == (string) rcube_addressbook::TYPE_TRUSTED_SENDER)\r\n                ) {\r\n                    continue;\r\n                }\r\n\r\n                $active = in_array($source['id'], (array) $this->rc->config->get('calendar_birthday_adressbooks')) ? $source['id'] : '';\r\n                $sources[] = html::tag(\r\n                    'li',\r\n                    null,\r\n                    html::label(\r\n                        null,\r\n                        $checkbox->show($active, ['value' => $source['id']])\r\n                        . rcube::Q(!empty($source['realname']) ? $source['realname'] : $source['name'])\r\n                    )\r\n                );\r\n            }\r\n\r\n            $p['blocks']['birthdays']['options']['birthday_adressbooks'] = [\r\n                'title'   => rcube::Q($this->gettext('birthdayscalendarsources')),\r\n                'content' => html::tag('ul', 'proplist', implode(\"\\n\", $sources)),\r\n            ];\r\n\r\n            $field_id = 'rcmfd_birthdays_alarm';\r\n            $select_type = new html_select(['name' => '_birthdays_alarm_type', 'id' => $field_id] + $input_attrib);\r\n            $select_type->add($this->gettext('none'), '');\r\n\r\n            foreach ($this->driver->alarm_types as $type) {\r\n                $select_type->add($this->rc->gettext(strtolower(\"alarm{$type}option\"), 'libcalendaring'), $type);\r\n            }\r\n\r\n            $input_value   = new html_inputfield(['name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3] + $input_attrib);\r\n            $select_offset = new html_select(['name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset'] + $input_attrib);\r\n\r\n            foreach (['-M','-H','-D'] as $trigger) {\r\n                $select_offset->add($this->rc->gettext('trigger' . $trigger, 'libcalendaring'), $trigger);\r\n            }\r\n\r\n            $preset      = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D'));\r\n            $preset_type = $this->rc->config->get('calendar_birthdays_alarm_type', '');\r\n\r\n            $p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = [\r\n                'title'   => html::label($field_id, rcube::Q($this->gettext('showalarms'))),\r\n                'content' => html::div(\r\n                    'input-group',\r\n                    $select_type->show($preset_type)\r\n                    . $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1])\r\n                ),\r\n            ];\r\n        }\r\n\r\n        return $p;\r\n    }\r\n\r\n    \/**\r\n     * Handler for preferences_save hook.\r\n     * Executed on Calendar settings form submit.\r\n     *\r\n     * @param array $p Original parameters\r\n     *\r\n     * @return array Modified parameters\r\n     *\/\r\n    public function preferences_save($p)\r\n    {\r\n        if ($p['section'] == 'calendar') {\r\n            $this->load_driver();\r\n\r\n            \/\/ compose default alarm preset value\r\n            $alarm_offset  = rcube_utils::get_input_value('_alarm_offset', rcube_utils::INPUT_POST);\r\n            $alarm_value   = rcube_utils::get_input_value('_alarm_value', rcube_utils::INPUT_POST);\r\n            $default_alarm = $alarm_offset[0] . intval($alarm_value) . $alarm_offset[1];\r\n\r\n            $birthdays_alarm_offset = rcube_utils::get_input_value('_birthdays_alarm_offset', rcube_utils::INPUT_POST);\r\n            $birthdays_alarm_value  = rcube_utils::get_input_value('_birthdays_alarm_value', rcube_utils::INPUT_POST);\r\n            $birthdays_alarm_value  = $birthdays_alarm_offset[0] . intval($birthdays_alarm_value) . $birthdays_alarm_offset[1];\r\n\r\n            $p['prefs'] = [\r\n                'calendar_default_view' => rcube_utils::get_input_value('_default_view', rcube_utils::INPUT_POST),\r\n                'calendar_timeslots'    => intval(rcube_utils::get_input_value('_timeslots', rcube_utils::INPUT_POST)),\r\n                'calendar_first_day'    => intval(rcube_utils::get_input_value('_first_day', rcube_utils::INPUT_POST)),\r\n                'calendar_first_hour'   => intval(rcube_utils::get_input_value('_first_hour', rcube_utils::INPUT_POST)),\r\n                'calendar_work_start'   => intval(rcube_utils::get_input_value('_work_start', rcube_utils::INPUT_POST)),\r\n                'calendar_work_end'     => intval(rcube_utils::get_input_value('_work_end', rcube_utils::INPUT_POST)),\r\n                'calendar_show_weekno'  => intval(rcube_utils::get_input_value('_show_weekno', rcube_utils::INPUT_POST)),\r\n                'calendar_event_coloring'       => intval(rcube_utils::get_input_value('_event_coloring', rcube_utils::INPUT_POST)),\r\n                'calendar_default_alarm_type'   => rcube_utils::get_input_value('_alarm_type', rcube_utils::INPUT_POST),\r\n                'calendar_default_alarm_offset' => $default_alarm,\r\n                'calendar_default_calendar'     => rcube_utils::get_input_value('_default_calendar', rcube_utils::INPUT_POST),\r\n                'calendar_date_format'          => null,  \/\/ clear previously saved values\r\n                'calendar_time_format'          => null,\r\n                'calendar_contact_birthdays'      => (bool) rcube_utils::get_input_value('_contact_birthdays', rcube_utils::INPUT_POST),\r\n                'calendar_birthday_adressbooks'   => (array) rcube_utils::get_input_value('_birthday_adressbooks', rcube_utils::INPUT_POST),\r\n                'calendar_birthdays_alarm_type'   => rcube_utils::get_input_value('_birthdays_alarm_type', rcube_utils::INPUT_POST),\r\n                'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null,\r\n                'calendar_itip_after_action'      => intval(rcube_utils::get_input_value('_after_action', rcube_utils::INPUT_POST)),\r\n            ];\r\n\r\n            if ($p['prefs']['calendar_itip_after_action'] == 4) {\r\n                $p['prefs']['calendar_itip_after_action'] = rcube_utils::get_input_value('_after_action_folder', rcube_utils::INPUT_POST, true);\r\n            }\r\n\r\n            \/\/ categories\r\n            if (empty($this->driver->nocategories)) {\r\n                $old_categories = $new_categories = [];\r\n\r\n                foreach ($this->driver->list_categories() as $name => $color) {\r\n                    $old_categories[md5($name)] = $name;\r\n                }\r\n\r\n                $categories = (array) rcube_utils::get_input_value('_categories', rcube_utils::INPUT_POST);\r\n                $colors     = (array) rcube_utils::get_input_value('_colors', rcube_utils::INPUT_POST);\r\n\r\n                foreach ($categories as $key => $name) {\r\n                    if (!isset($colors[$key])) {\r\n                        continue;\r\n                    }\r\n\r\n                    $color = preg_replace('\/^#\/', '', strval($colors[$key]));\r\n\r\n                    \/\/ rename categories in existing events -> driver's job\r\n                    if (!empty($old_categories[$key])) {\r\n                        $oldname = $old_categories[$key];\r\n                        $this->driver->replace_category($oldname, $name, $color);\r\n                        unset($old_categories[$key]);\r\n                    } else {\r\n                        $this->driver->add_category($name, $color);\r\n                    }\r\n\r\n                    $new_categories[$name] = $color;\r\n                }\r\n\r\n                \/\/ these old categories have been removed, alter events accordingly -> driver's job\r\n                foreach ((array) $old_categories as $key => $name) {\r\n                    $this->driver->remove_category($name);\r\n                }\r\n\r\n                $p['prefs']['calendar_categories'] = $new_categories;\r\n            }\r\n        }\r\n\r\n        return $p;\r\n    }\r\n\r\n    \/**\r\n     * Dispatcher for calendar actions initiated by the client\r\n     *\/\r\n    public function calendar_action()\r\n    {\r\n        $action  = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);\r\n        $cal     = rcube_utils::get_input_value('c', rcube_utils::INPUT_GPC);\r\n        $success = false;\r\n        $reload  = false;\r\n        $jsenv = [];\r\n\r\n        if (isset($cal['showalarms'])) {\r\n            $cal['showalarms'] = intval($cal['showalarms']);\r\n        }\r\n\r\n        switch ($action) {\r\n            case \"form-new\":\r\n            case \"form-edit\":\r\n                echo $this->ui->calendar_editform($action, $cal);\r\n                exit;\r\n\r\n            case \"new\":\r\n                $success = $this->driver->create_calendar($cal);\r\n                $reload  = true;\r\n                break;\r\n\r\n            case \"edit\":\r\n                $success = $this->driver->edit_calendar($cal);\r\n                $reload  = true;\r\n                break;\r\n\r\n            case \"delete\":\r\n                if ($success = $this->driver->delete_calendar($cal)) {\r\n                    $this->rc->output->command('plugin.destroy_source', ['id' => $cal['id']]);\r\n                }\r\n                break;\r\n\r\n            case \"subscribe\":\r\n                if (!$this->driver->subscribe_calendar($cal)) {\r\n                    $this->rc->output->show_message($this->gettext('errorsaving'), 'error');\r\n                } else {\r\n                    $calendars = $this->driver->list_calendars();\r\n                    $calendar  = !empty($calendars[$cal['id']]) ? $calendars[$cal['id']] : null;\r\n\r\n                    \/\/ find parent folder and check if it's a \"user calendar\"\r\n                    \/\/ if it's also activated we need to refresh it (#5340)\r\n                    while (!empty($calendar['parent'])) {\r\n                        if (isset($calendars[$calendar['parent']])) {\r\n                            $calendar = $calendars[$calendar['parent']];\r\n                        } else {\r\n                            break;\r\n                        }\r\n                    }\r\n\r\n                    if ($calendar && $calendar['id'] != $cal['id']\r\n                        && !empty($calendar['active'])\r\n                        && $calendar['group'] == \"other user\"\r\n                    ) {\r\n                        $this->rc->output->command('plugin.refresh_source', $calendar['id']);\r\n                    }\r\n                }\r\n                return;\r\n\r\n            case \"search\":\r\n                $results    = [];\r\n                $color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);\r\n                $query      = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);\r\n                $source     = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);\r\n\r\n                foreach ((array) $this->driver->search_calendars($query, $source) as $id => $prop) {\r\n                    $editname = $prop['editname'] ?? '';\r\n                    unset($prop['editname']);  \/\/ force full name to be displayed\r\n                    $prop['active'] = false;\r\n\r\n                    \/\/ let the UI generate HTML and CSS representation for this calendar\r\n                    $html = $this->ui->calendar_list_item($id, $prop, $jsenv);\r\n                    $cal  = $jsenv[$id] ?? []; \/\/ @phpstan-ignore-line\r\n                    $cal['editname'] = $editname;\r\n                    $cal['html']     = $html;\r\n\r\n                    if (!empty($prop['color'])) {\r\n                        $cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode);\r\n                    }\r\n\r\n                    $results[] = $cal;\r\n                }\r\n\r\n                \/\/ report more results available\r\n                if (!empty($this->driver->search_more_results)) {\r\n                    $this->rc->output->show_message('autocompletemore', 'notice');\r\n                }\r\n\r\n                $reqid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);\r\n                $this->rc->output->command('multi_thread_http_response', $results, $reqid);\r\n                return;\r\n        }\r\n\r\n        if ($success) {\r\n            $this->rc->output->show_message('successfullysaved', 'confirmation');\r\n        } else {\r\n            $error_msg = $this->gettext('errorsaving');\r\n            if (!empty($this->driver->last_error)) {\r\n                $error_msg .= ': ' . $this->driver->last_error;\r\n            }\r\n            $this->rc->output->show_message($error_msg, 'error');\r\n        }\r\n\r\n        $this->rc->output->command('plugin.unlock_saving');\r\n\r\n        if ($success && $reload) {\r\n            $this->rc->output->command('plugin.reload_view');\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Dispatcher for event actions initiated by the client\r\n     *\/\r\n    public function event_action()\r\n    {\r\n        $action  = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);\r\n        $event   = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true);\r\n        $success = $reload = $got_msg = false;\r\n        $old     = null;\r\n\r\n        \/\/ read old event data in order to find changes\r\n        if ((!empty($event['_notify']) || !empty($event['_decline'])) && $action != 'new') {\r\n            $old = $this->driver->get_event($event);\r\n\r\n            \/\/ load main event if savemode is 'all' or if deleting 'future' events\r\n            if (!empty($old['recurrence_id'])\r\n                && !empty($event['_savemode'])\r\n                && ($event['_savemode'] == 'all' || ($event['_savemode'] == 'future' && $action == 'remove' && empty($event['_decline'])))\r\n            ) {\r\n                $old['id'] = $old['recurrence_id'];\r\n                $old = $this->driver->get_event($old);\r\n            }\r\n        }\r\n\r\n        switch ($action) {\r\n            case \"new\":\r\n                \/\/ create UID for new event\r\n                $event['uid'] = $this->generate_uid();\r\n                if (!$this->write_preprocess($event, $action)) {\r\n                    $got_msg = true;\r\n                } elseif ($success = $this->driver->new_event($event)) {\r\n                    $event['id']        = $event['uid'];\r\n                    $event['_savemode'] = 'all';\r\n\r\n                    $this->cleanup_event($event);\r\n                    $this->event_save_success($event, null, $action, true);\r\n                    $this->talk_room_update($event);\r\n                }\r\n\r\n                $reload = $success && !empty($event['recurrence']) ? 2 : 1;\r\n                break;\r\n\r\n            case \"edit\":\r\n                if (!$this->write_preprocess($event, $action)) {\r\n                    $got_msg = true;\r\n                } elseif ($success = $this->driver->edit_event($event)) {\r\n                    $this->cleanup_event($event);\r\n                    $this->event_save_success($event, $old, $action, $success);\r\n                    $this->talk_room_update($event);\r\n                }\r\n\r\n                $reload = $success && (!empty($event['recurrence']) || !empty($event['_savemode']) || !empty($event['_fromcalendar'])) ? 2 : 1;\r\n                break;\r\n\r\n            case \"resize\":\r\n                if (!$this->write_preprocess($event, $action)) {\r\n                    $got_msg = true;\r\n                } elseif ($success = $this->driver->resize_event($event)) {\r\n                    $this->event_save_success($event, $old, $action, $success);\r\n                }\r\n\r\n                $reload = !empty($event['_savemode']) ? 2 : 1;\r\n                break;\r\n\r\n            case \"move\":\r\n                if (!$this->write_preprocess($event, $action)) {\r\n                    $got_msg = true;\r\n                } elseif ($success = $this->driver->move_event($event)) {\r\n                    $this->event_save_success($event, $old, $action, $success);\r\n                }\r\n\r\n                $reload = $success && !empty($event['_savemode']) ? 2 : 1;\r\n                break;\r\n\r\n            case \"remove\":\r\n                \/\/ remove previous deletes\r\n                $undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;\r\n\r\n                \/\/ search for event if only UID is given\r\n                if (!isset($event['calendar']) && !empty($event['uid'])) {\r\n                    if (!($event = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE))) {\r\n                        break;\r\n                    }\r\n                    $undo_time = 0;\r\n                }\r\n\r\n                \/\/ Note: the driver is responsible for setting $_SESSION['calendar_event_undo']\r\n                \/\/       containing 'ts' and 'data' elements\r\n                $success = $this->driver->remove_event($event, $undo_time < 1);\r\n                $reload = (!$success || !empty($event['_savemode'])) ? 2 : 1;\r\n\r\n                if ($undo_time > 0 && $success) {\r\n                    \/\/ display message with Undo link.\r\n                    $onclick = sprintf(\r\n                        \"%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))\",\r\n                        rcmail_output::JS_OBJECT_NAME,\r\n                        rcmail_output::JS_OBJECT_NAME\r\n                    );\r\n                    $msg = html::span(null, $this->gettext('successremoval'))\r\n                        . ' ' . html::a(['onclick' => $onclick], $this->gettext('undo'));\r\n\r\n                    $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);\r\n                    $got_msg = true;\r\n                } elseif ($success) {\r\n                    $this->rc->output->show_message('calendar.successremoval', 'confirmation');\r\n                    $got_msg = true;\r\n                }\r\n\r\n                \/\/ send cancellation for the main event\r\n                if (isset($event['_savemode']) && $event['_savemode'] == 'all') {\r\n                    unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']);\r\n                }\r\n                \/\/ send an update for the main event's recurrence rule instead of a cancellation message\r\n                elseif (isset($event['_savemode']) && $event['_savemode'] == 'future' && !is_bool($success)) {\r\n                    $event['_savemode'] = 'all';  \/\/ force event_save_success() to load master event\r\n                    $action  = 'edit';\r\n                    $success = true;\r\n                }\r\n\r\n                \/\/ send iTIP reply that participant has declined the event\r\n                if ($success && !empty($event['_decline'])) {\r\n                    $emails    = $this->get_user_emails();\r\n                    $organizer = null;\r\n                    $reply_sender = null;\r\n\r\n                    foreach ($old['attendees'] as $i => $attendee) {\r\n                        if ($attendee['role'] == 'ORGANIZER') {\r\n                            $organizer = $attendee;\r\n                        } elseif (!empty($attendee['email']) && in_array(strtolower($attendee['email']), $emails)) {\r\n                            $old['attendees'][$i]['status'] = 'DECLINED';\r\n                            $reply_sender = $attendee['email'];\r\n                        }\r\n                    }\r\n\r\n                    if ($event['_savemode'] == 'future' && $event['id'] != $old['id']) {\r\n                        $old['thisandfuture'] = true;\r\n                    }\r\n\r\n                    $itip = $this->load_itip();\r\n                    $itip->set_sender_email($reply_sender);\r\n\r\n                    if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined')) {\r\n                        $mailto = !empty($organizer['name']) ? $organizer['name'] : $organizer['email'];\r\n                        $msg    = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);\r\n\r\n                        $this->rc->output->command('display_message', $msg, 'confirmation');\r\n                    } else {\r\n                        $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');\r\n                    }\r\n                } elseif ($success) {\r\n                    $this->event_save_success($event, $old, $action, $success);\r\n                }\r\n\r\n                break;\r\n\r\n            case \"undo\":\r\n                \/\/ Restore deleted event\r\n                if (!empty($_SESSION['calendar_event_undo']['data'])) {\r\n                    $event   = $_SESSION['calendar_event_undo']['data'];\r\n                    $success = $this->driver->restore_event($event);\r\n                }\r\n\r\n                if ($success) {\r\n                    $this->rc->session->remove('calendar_event_undo');\r\n                    $this->rc->output->show_message('calendar.successrestore', 'confirmation');\r\n                    $got_msg = true;\r\n                    $reload  = 2;\r\n                }\r\n\r\n                break;\r\n\r\n            case \"rsvp\":\r\n                $itip_sending  = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);\r\n                $status        = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST);\r\n                $attendees     = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST);\r\n                $reply_comment = $event['comment'];\r\n\r\n                $this->write_preprocess($event, 'edit');\r\n                $ev = $this->driver->get_event($event);\r\n                $ev['attendees'] = $event['attendees'];\r\n                $ev['free_busy'] = $event['free_busy'];\r\n                $ev['_savemode'] = $event['_savemode'];\r\n                $ev['comment']   = $reply_comment;\r\n\r\n                \/\/ send invitation to delegatee + add it as attendee\r\n                if ($status == 'delegated' && !empty($event['to'])) {\r\n                    $itip = $this->load_itip();\r\n                    if ($itip->delegate_to($ev, $event['to'], !empty($event['rsvp']), $attendees)) {\r\n                        $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');\r\n                        $noreply = false;\r\n                    }\r\n                }\r\n\r\n                $event = $ev;\r\n\r\n                \/\/ compose a list of attendees affected by this change\r\n                $updated_attendees = array_filter(array_map(\r\n                    function ($j) use ($event) {\r\n                        return $event['attendees'][$j];\r\n                    },\r\n                    $attendees\r\n                ));\r\n\r\n                if ($success = $this->driver->edit_rsvp($event, $status, $updated_attendees)) {\r\n                    $noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC);\r\n                    $noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;\r\n                    $reload  = $event['calendar'] != $ev['calendar'] || !empty($event['recurrence']) ? 2 : 1;\r\n                    $emails  = $this->get_user_emails();\r\n                    $ownedResourceEmails = $this->owned_resources_emails();\r\n                    $organizer = null;\r\n                    $resourceConfirmation = false;\r\n                    $reply_sender = null;\r\n\r\n                    foreach ($event['attendees'] as $i => $attendee) {\r\n                        if (($attendee['role'] ?? '') == 'ORGANIZER') {\r\n                            $organizer = $attendee;\r\n                        } elseif (!empty($attendee['email']) && in_array_nocase($attendee['email'], $emails)) {\r\n                            $reply_sender = $attendee['email'];\r\n                        } elseif (!empty($attendee['cutype']) && $attendee['cutype'] == 'RESOURCE' && !empty($attendee['email']) && in_array_nocase($attendee['email'], $ownedResourceEmails)) {\r\n                            $resourceConfirmation = true;\r\n                            \/\/ Note on behalf of which resource this update is going to be sent out\r\n                            $event['_resource'] = $attendee['email'];\r\n                        }\r\n                    }\r\n\r\n                    if (!$noreply) {\r\n                        $itip = $this->load_itip();\r\n                        $itip->set_sender_email($reply_sender);\r\n                        $event['thisandfuture'] = $event['_savemode'] == 'future';\r\n                        $bodytextprefix = $resourceConfirmation ? 'itipmailbodyresource' : 'itipmailbody';\r\n\r\n                        if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, $bodytextprefix . $status)) {\r\n                            $mailto = !empty($organizer['name']) ? $organizer['name'] : $organizer['email'];\r\n                            $msg    = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);\r\n\r\n                            $this->rc->output->command('display_message', $msg, 'confirmation');\r\n                        } else {\r\n                            $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');\r\n                        }\r\n                    }\r\n\r\n                    \/\/ refresh all calendars\r\n                    if ($event['calendar'] != $ev['calendar']) {\r\n                        $this->rc->output->command('plugin.refresh_calendar', ['source' => null, 'refetch' => true]);\r\n                        $reload = 0;\r\n                    }\r\n                }\r\n\r\n                break;\r\n\r\n            case \"dismiss\":\r\n                $event['ids'] = explode(',', $event['id']);\r\n                $plugin  = $this->rc->plugins->exec_hook('dismiss_alarms', $event);\r\n                $success = $plugin['success'];\r\n\r\n                foreach ($event['ids'] as $id) {\r\n                    if (strpos($id, 'cal:') === 0) {\r\n                        $success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']);\r\n                    }\r\n                }\r\n\r\n                break;\r\n\r\n            case \"changelog\":\r\n                $data = $this->driver->get_event_changelog($event);\r\n                if (is_array($data) && !empty($data)) {\r\n                    $lib = $this->lib;\r\n                    $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');\r\n                    array_walk($data, function (&$change) use ($lib, $dtformat) {\r\n                        if (!empty($change['date'])) {\r\n                            $dt = $lib->adjust_timezone($change['date']);\r\n\r\n                            if ($dt instanceof DateTimeInterface) {\r\n                                $change['date'] = $this->rc->format_date($dt, $dtformat, false);\r\n                            }\r\n                        }\r\n                    });\r\n\r\n                    $this->rc->output->command('plugin.render_event_changelog', $data);\r\n                } else {\r\n                    $this->rc->output->command('plugin.render_event_changelog', false);\r\n                }\r\n\r\n                $got_msg = true;\r\n                $reload  = false;\r\n\r\n                break;\r\n\r\n            case \"diff\":\r\n                $data = $this->driver->get_event_diff($event, $event['rev1'], $event['rev2']);\r\n                if (is_array($data)) {\r\n                    \/\/ convert some properties, similar to self::_client_event()\r\n                    $lib = $this->lib;\r\n                    array_walk($data['changes'], function (&$change, $i) use ($lib) {\r\n                        \/\/ convert date cols\r\n                        foreach (['start', 'end', 'created', 'changed'] as $col) {\r\n                            if ($change['property'] == $col) {\r\n                                $change['old'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c');\r\n                                $change['new'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c');\r\n                            }\r\n                        }\r\n                        \/\/ create textual representation for alarms and recurrence\r\n                        if ($change['property'] == 'alarms') {\r\n                            if (is_array($change['old'])) {\r\n                                $change['old_'] = libcalendaring::alarm_text($change['old']);\r\n                            }\r\n                            if (is_array($change['new'])) {\r\n                                $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));\r\n                            }\r\n                        }\r\n                        if ($change['property'] == 'recurrence') {\r\n                            if (is_array($change['old'])) {\r\n                                $change['old_'] = $lib->recurrence_text($change['old']);\r\n                            }\r\n                            if (is_array($change['new'])) {\r\n                                $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));\r\n                            }\r\n                        }\r\n                        if ($change['property'] == 'attachments') {\r\n                            if (is_array($change['old'])) {\r\n                                $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);\r\n                            }\r\n                            if (is_array($change['new'])) {\r\n                                $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);\r\n                            }\r\n                        }\r\n                        \/\/ compute a nice diff of description texts\r\n                        if ($change['property'] == 'description') {\r\n                            $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);\r\n                        }\r\n                    });\r\n\r\n                    $this->rc->output->command('plugin.event_show_diff', $data);\r\n                } else {\r\n                    $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');\r\n                }\r\n\r\n                $got_msg = true;\r\n                $reload  = false;\r\n\r\n                break;\r\n\r\n            case \"show\":\r\n                if ($event = $this->driver->get_event_revison($event, $event['rev'])) {\r\n                    $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event));\r\n                } else {\r\n                    $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');\r\n                }\r\n\r\n                $got_msg = true;\r\n                $reload  = false;\r\n                break;\r\n\r\n            case \"restore\":\r\n                if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {\r\n                    $_event = $this->driver->get_event($event);\r\n                    $reload = $_event['recurrence'] ? 2 : 1;\r\n                    $msg = $this->gettext(['name' => 'objectrestoresuccess', 'vars' => ['rev' => $event['rev']]]);\r\n                    $this->rc->output->command('display_message', $msg, 'confirmation');\r\n                    $this->rc->output->command('plugin.close_history_dialog');\r\n                } else {\r\n                    $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');\r\n                    $reload = 0;\r\n                }\r\n\r\n                $got_msg = true;\r\n                break;\r\n        }\r\n\r\n        \/\/ show confirmation\/error message\r\n        if (!$got_msg) {\r\n            if ($success) {\r\n                $this->rc->output->show_message('successfullysaved', 'confirmation');\r\n            } else {\r\n                $this->rc->output->show_message('calendar.errorsaving', 'error');\r\n            }\r\n        }\r\n\r\n        \/\/ unlock client\r\n        $this->rc->output->command('plugin.unlock_saving', $success);\r\n\r\n        \/\/ update event object on the client or trigger a complete refresh if too complicated\r\n        if ($reload && empty($_REQUEST['_framed'])) {\r\n            $args = ['source' => $event['calendar']];\r\n            if ($reload > 1) {\r\n                $args['refetch'] = true;\r\n            } elseif ($success && $action != 'remove') {\r\n                $args['update'] = $this->_client_event($this->driver->get_event($event), true);\r\n            }\r\n            $this->rc->output->command('plugin.refresh_calendar', $args);\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Helper method sending iTip notifications after successful event updates\r\n     *\/\r\n    private function event_save_success(&$event, $old, $action, $success)\r\n    {\r\n        \/\/ $success is a new event ID\r\n        if ($success !== true) {\r\n            \/\/ send update notification on the main event\r\n            if (!empty($event['_savemode']) && $event['_savemode'] == 'future' && !empty($event['_notify'])\r\n                && !empty($old['attendees']) && !empty($old['recurrence_id'])\r\n            ) {\r\n                $master = $this->driver->get_event(['id' => $old['recurrence_id'], 'calendar' => $old['calendar']], 0, true);\r\n                unset($master['_instance'], $master['recurrence_date']);\r\n\r\n                $sent = $this->notify_attendees($master, null, $action, $event['_comment'], false);\r\n                if ($sent < 0) {\r\n                    $this->rc->output->show_message('calendar.errornotifying', 'error');\r\n                }\r\n\r\n                $event['attendees'] = $master['attendees'];  \/\/ this tricks us into the next if clause\r\n            }\r\n\r\n            \/\/ delete old reference if saved as new\r\n            if (!empty($event['_savemode']) && ($event['_savemode'] == 'future' || $event['_savemode'] == 'new')) {\r\n                $old = null;\r\n            }\r\n\r\n            $event['id']        = $success;\r\n            $event['_savemode'] = 'all';\r\n        }\r\n\r\n        \/\/ send out notifications\r\n        if (!empty($event['_notify']) && (!empty($event['attendees']) || !empty($old['attendees']))) {\r\n            $_savemode = $event['_savemode'] ?? null;\r\n\r\n            \/\/ send notification for the main event when savemode is 'all'\r\n            if ($action != 'remove' && $_savemode == 'all'\r\n                && (!empty($event['recurrence_id']) || !empty($old['recurrence_id']) || ($old && $old['id'] != $event['id']))\r\n            ) {\r\n                if (!empty($event['recurrence_id'])) {\r\n                    $event['id'] = $event['recurrence_id'];\r\n                } elseif (!empty($old['recurrence_id'])) {\r\n                    $event['id'] = $old['recurrence_id'];\r\n                } else {\r\n                    $event['id'] = $old['id'];\r\n                }\r\n                $event = $this->driver->get_event($event, 0, true);\r\n                unset($event['_instance'], $event['recurrence_date']);\r\n            } else {\r\n                \/\/ make sure we have the complete record\r\n                $event = $action == 'remove' ? $old : $this->driver->get_event($event, 0, true);\r\n            }\r\n\r\n            $event['_savemode'] = $_savemode;\r\n\r\n            if ($old) {\r\n                $old['thisandfuture'] = $_savemode == 'future';\r\n            }\r\n\r\n            \/\/ only notify if data really changed (TODO: do diff check on client already)\r\n            if (!$old || $action == 'remove' || self::event_diff($event, $old)) {\r\n                $comment = $event['_comment'] ?? null;\r\n                $sent    = $this->notify_attendees($event, $old, $action, $comment);\r\n\r\n                if ($sent > 0) {\r\n                    $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');\r\n                } elseif ($sent < 0) {\r\n                    $this->rc->output->show_message('calendar.errornotifying', 'error');\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Handler for load-requests from fullcalendar\r\n     * This will return pure JSON formatted output\r\n     *\/\r\n    public function load_events()\r\n    {\r\n        $start  = $this->input_timestamp('start', rcube_utils::INPUT_GET);\r\n        $end    = $this->input_timestamp('end', rcube_utils::INPUT_GET);\r\n        $query  = rcube_utils::get_input_value('q', rcube_utils::INPUT_GET);\r\n        $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);\r\n\r\n        $events = $this->driver->load_events($start, $end, $query, $source);\r\n        echo $this->encode($events, !empty($query));\r\n        exit;\r\n    }\r\n\r\n    \/**\r\n     * Handler for requests fetching event counts for calendars\r\n     *\/\r\n    public function count_events()\r\n    {\r\n        \/\/ don't update session on these requests (avoiding race conditions)\r\n        $this->rc->session->nowrite = true;\r\n\r\n        $start  = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);\r\n        $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);\r\n        $end    = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET);\r\n\r\n        if (!$start) {\r\n            $start = new DateTime('today 00:00:00', $this->timezone);\r\n            $start = $start->format('U');\r\n        }\r\n\r\n        $counts = $this->driver->count_events($source, $start, $end);\r\n\r\n        $this->rc->output->command('plugin.update_counts', ['counts' => $counts]);\r\n    }\r\n\r\n    \/**\r\n     * Load event data from an iTip message attachment\r\n     *\/\r\n    public function itip_events($msgref)\r\n    {\r\n        $path = explode('\/', $msgref);\r\n        $msg  = array_pop($path);\r\n        $mbox = implode('\/', $path);\r\n        [$uid, $mime_id] = explode('#', $msg);\r\n        $events = [];\r\n\r\n        if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {\r\n            $partstat = 'NEEDS-ACTION';\r\n\r\n            $event['id']        = $event['uid'];\r\n            $event['temporary'] = true;\r\n            $event['readonly']  = true;\r\n            $event['calendar']  = '--invitation--itip';\r\n            $event['className'] = 'fc-invitation-' . strtolower($partstat);\r\n            $event['_mbox']     = $mbox;\r\n            $event['_uid']      = $uid;\r\n            $event['_part']     = $mime_id;\r\n\r\n            $events[] = $this->_client_event($event, true);\r\n\r\n            \/\/ add recurring instances\r\n            if (!empty($event['recurrence'])) {\r\n                \/\/ Some installations can't handle all occurrences (aborting the request w\/o an error in log)\r\n                $freq = !empty($event['recurrence']['FREQ']) ? $event['recurrence']['FREQ'] : null;\r\n                $end  = clone $event['start'];\r\n                $end->add(new DateInterval($freq == 'DAILY' ? 'P1Y' : 'P10Y'));\r\n\r\n                foreach ($this->driver->get_recurring_events($event, $event['start'], $end) as $recurring) {\r\n                    $recurring['temporary'] = true;\r\n                    $recurring['readonly']  = true;\r\n                    $recurring['calendar']  = '--invitation--itip';\r\n\r\n                    $events[] = $this->_client_event($recurring, true);\r\n                }\r\n            }\r\n        }\r\n\r\n        return $events;\r\n    }\r\n\r\n    \/**\r\n     * Handle invitations to a shared folder\r\n     *\/\r\n    public function share_invitation()\r\n    {\r\n        $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);\r\n        $invitation = rcube_utils::get_input_value('invitation', rcube_utils::INPUT_POST);\r\n\r\n        if ($calendar = $this->driver->accept_share_invitation($invitation)) {\r\n            $this->rc->output->command('plugin.share-invitation', ['id' => $id, 'source' => $calendar]);\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Handler for keep-alive requests\r\n     * This will check for updated data in active calendars and sync them to the client\r\n     *\/\r\n    public function refresh($attr)\r\n    {\r\n        \/\/ refresh the entire calendar every 10th time to also sync deleted events\r\n        if (rand(0, 10) == 10) {\r\n            $this->rc->output->command('plugin.refresh_calendar', ['refetch' => true]);\r\n            return;\r\n        }\r\n\r\n        $counts = [];\r\n\r\n        foreach ($this->driver->list_calendars(calendar_driver::FILTER_ACTIVE) as $cal) {\r\n            $events = $this->driver->load_events(\r\n                rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC),\r\n                rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC),\r\n                rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC),\r\n                $cal['id'],\r\n                1,\r\n                $attr['last']\r\n            );\r\n\r\n            foreach ($events as $event) {\r\n                $this->rc->output->command(\r\n                    'plugin.refresh_calendar',\r\n                    ['source' => $cal['id'], 'update' => $this->_client_event($event)]\r\n                );\r\n            }\r\n\r\n            \/\/ refresh count for this calendar\r\n            if (!empty($cal['counts'])) {\r\n                $today = new DateTime('today 00:00:00', $this->timezone);\r\n                $counts += $this->driver->count_events($cal['id'], $today->format('U'));\r\n            }\r\n        }\r\n\r\n        if (!empty($counts)) {\r\n            $this->rc->output->command('plugin.update_counts', ['counts' => $counts]);\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.\r\n     * This will check for pending notifications and pass them to the client\r\n     *\/\r\n    public function pending_alarms($p)\r\n    {\r\n        $this->load_driver();\r\n\r\n        $time = !empty($p['time']) ? $p['time'] : time();\r\n\r\n        if ($alarms = $this->driver->pending_alarms($time)) {\r\n            foreach ($alarms as $alarm) {\r\n                $alarm['id'] = 'cal:' . $alarm['id'];  \/\/ prefix ID with cal:\r\n                $p['alarms'][] = $alarm;\r\n            }\r\n        }\r\n\r\n        \/\/ get alarms for birthdays calendar\r\n        if (\r\n            $this->rc->config->get('calendar_contact_birthdays')\r\n            && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY'\r\n        ) {\r\n            $cache = $this->rc->get_cache('calendar.birthdayalarms', 'db');\r\n\r\n            foreach ($this->driver->load_birthday_events($time, $time + 86400 * 60) as $e) {\r\n                $alarm = libcalendaring::get_next_alarm($e);\r\n\r\n                \/\/ overwrite alarm time with snooze value (or null if dismissed)\r\n                if ($dismissed = $cache->get($e['id'])) {\r\n                    $alarm['time'] = $dismissed['notifyat'];\r\n                }\r\n\r\n                \/\/ add to list if alarm is set\r\n                if ($alarm && !empty($alarm['time']) && $alarm['time'] <= $time) {\r\n                    $e['id']       = 'cal:bday:' . $e['id'];\r\n                    $e['notifyat'] = $alarm['time'];\r\n                    $p['alarms'][] = $e;\r\n                }\r\n            }\r\n        }\r\n\r\n        return $p;\r\n    }\r\n\r\n    \/**\r\n     * Handler for alarm dismiss hook triggered by libcalendaring\r\n     *\/\r\n    public function dismiss_alarms($p)\r\n    {\r\n        $this->load_driver();\r\n\r\n        foreach ((array) $p['ids'] as $id) {\r\n            if (strpos($id, 'cal:bday:') === 0) {\r\n                $p['success'] |= $this->driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']);\r\n            } elseif (strpos($id, 'cal:') === 0) {\r\n                $p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']);\r\n            }\r\n        }\r\n\r\n        return $p;\r\n    }\r\n\r\n    \/**\r\n     * Handler for check-recent requests which are accidentally sent to calendar\r\n     *\/\r\n    public function check_recent()\r\n    {\r\n        \/\/ NOP\r\n        $this->rc->output->send();\r\n    }\r\n\r\n    \/**\r\n     * Hook triggered when a contact is saved\r\n     *\/\r\n    public function contact_update($p)\r\n    {\r\n        \/\/ clear birthdays calendar cache\r\n        if (!empty($p['record']['birthday'])) {\r\n            $cache = $this->rc->get_cache('calendar.birthdays', 'db');\r\n            $cache->remove();\r\n        }\r\n    }\r\n\r\n    \/**\r\n     *\r\n     *\/\r\n    public function import_events()\r\n    {\r\n        \/\/ Upload progress update\r\n        if (!empty($_GET['_progress'])) {\r\n            $this->rc->upload_progress();\r\n        }\r\n\r\n        @set_time_limit(0);\r\n\r\n        \/\/ process uploaded file if there is no error\r\n        $err = $_FILES['_data']['error'];\r\n\r\n        if (!$err && !empty($_FILES['_data']['tmp_name'])) {\r\n            $calendar   = rcube_utils::get_input_value('calendar', rcube_utils::INPUT_GPC);\r\n            $rangestart = !empty($_REQUEST['_range']) ? date_create(\"now -\" . intval($_REQUEST['_range']) . \" months\") : 0;\r\n\r\n            \/\/ extract zip file\r\n            if ($_FILES['_data']['type'] == 'application\/zip') {\r\n                $count = 0;\r\n                if (class_exists('ZipArchive', false)) {\r\n                    $zip = new ZipArchive();\r\n                    if ($zip->open($_FILES['_data']['tmp_name'])) {\r\n                        $randname = uniqid('zip-' . session_id(), true);\r\n                        $tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname;\r\n                        mkdir($tmpdir, 0700);\r\n\r\n                        \/\/ extract each ical file from the archive and import it\r\n                        for ($i = 0; $i < $zip->numFiles; $i++) {\r\n                            $filename = $zip->getNameIndex($i);\r\n                            if (preg_match('\/\\.ics$\/i', $filename)) {\r\n                                $tmpfile = $tmpdir . '\/' . basename($filename);\r\n                                if (copy('zip:\/\/' . $_FILES['_data']['tmp_name'] . '#' . $filename, $tmpfile)) {\r\n                                    $count += $this->import_from_file($tmpfile, $calendar, $rangestart, $errors);\r\n                                    unlink($tmpfile);\r\n                                }\r\n                            }\r\n                        }\r\n\r\n                        rmdir($tmpdir);\r\n                        $zip->close();\r\n                    } else {\r\n                        $errors = 1;\r\n                        $msg = 'Failed to open zip file.';\r\n                    }\r\n                } else {\r\n                    $errors = 1;\r\n                    $msg = 'Zip files are not supported for import.';\r\n                }\r\n            } else {\r\n                \/\/ attempt to import teh uploaded file directly\r\n                $count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $errors);\r\n            }\r\n\r\n            if ($count) {\r\n                $this->rc->output->command('display_message', $this->gettext(['name' => 'importsuccess', 'vars' => ['nr' => $count]]), 'confirmation');\r\n                $this->rc->output->command('plugin.import_success', ['source' => $calendar, 'refetch' => true]);\r\n            } elseif (empty($errors)) {\r\n                $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');\r\n                $this->rc->output->command('plugin.import_success', ['source' => $calendar]);\r\n            } else {\r\n                $this->rc->output->command('plugin.import_error', ['message' => $this->gettext('importerror')\r\n                    . (!empty($msg) ? ': ' . $msg : '')]);\r\n            }\r\n        } else {\r\n            if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {\r\n                $max = $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize')));\r\n                $msg = $this->rc->gettext(['name' => 'filesizeerror', 'vars' => ['size' => $max]]);\r\n            } else {\r\n                $msg = $this->rc->gettext('fileuploaderror');\r\n            }\r\n\r\n            $this->rc->output->command('plugin.import_error', ['message' => $msg]);\r\n        }\r\n\r\n        $this->rc->output->send('iframe');\r\n    }\r\n\r\n    \/**\r\n     * Helper function to parse and import a single .ics file\r\n     *\/\r\n    private function import_from_file($filepath, $calendar, $rangestart, &$errors)\r\n    {\r\n        $user_email = $this->rc->user->get_username();\r\n        $ical       = $this->get_ical();\r\n        $errors     = !$ical->fopen($filepath);\r\n\r\n        $count = $i = 0;\r\n\r\n        foreach ($ical as $event) {\r\n            \/\/ keep the browser connection alive on long import jobs\r\n            if (++$i > 100 && $i % 100 == 0) {\r\n                echo \"<!-- -->\";\r\n                ob_flush();\r\n            }\r\n\r\n            \/\/ TODO: correctly handle recurring events which start before $rangestart\r\n            if ($rangestart && $event['end'] < $rangestart\r\n                && (empty($event['recurrence']) || (!empty($event['recurrence']['until']) && $event['recurrence']['until'] < $rangestart))\r\n            ) {\r\n                continue;\r\n            }\r\n\r\n            $event['_owner']   = $user_email;\r\n            $event['calendar'] = $calendar;\r\n\r\n            if ($this->driver->new_event($event)) {\r\n                $count++;\r\n            } else {\r\n                $errors++;\r\n            }\r\n        }\r\n\r\n        return $count;\r\n    }\r\n\r\n    \/**\r\n     * Construct the ics file for exporting events to iCalendar format;\r\n     *\/\r\n    public function export_events($terminate = true)\r\n    {\r\n        $start       = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);\r\n        $end         = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET);\r\n        $event_id    = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET);\r\n        $attachments = rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GET);\r\n        $calid       = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);\r\n\r\n        if (!isset($start)) {\r\n            $start = 'today -1 year';\r\n        }\r\n        if (!is_numeric($start)) {\r\n            $start = strtotime($start . ' 00:00:00');\r\n        }\r\n        if (!$end) {\r\n            $end = 'today +10 years';\r\n        }\r\n        if (!is_numeric($end)) {\r\n            $end = strtotime($end . ' 23:59:59');\r\n        }\r\n\r\n        $filename  = $calid;\r\n        $calendars = $this->driver->list_calendars();\r\n        $events    = [];\r\n\r\n        if (!empty($calendars[$calid])) {\r\n            $filename = !empty($calendars[$calid]['name']) ? $calendars[$calid]['name'] : $calid;\r\n            $filename = asciiwords(html_entity_decode($filename));  \/\/ to 7bit ascii\r\n\r\n            if (!empty($event_id)) {\r\n                if ($event = $this->driver->get_event(['calendar' => $calid, 'id' => $event_id], 0, true)) {\r\n                    if (!empty($event['recurrence_id'])) {\r\n                        $event = $this->driver->get_event(['calendar' => $calid, 'id' => $event['recurrence_id']], 0, true);\r\n                    }\r\n\r\n                    $events   = [$event];\r\n                    $filename = asciiwords($event['title']);\r\n\r\n                    if (empty($filename)) {\r\n                        $filename = 'event';\r\n                    }\r\n                }\r\n            } else {\r\n                $events = $this->driver->load_events($start, $end, null, $calid, 0);\r\n                if (empty($filename)) {\r\n                    $filename = $calid;\r\n                }\r\n            }\r\n        }\r\n\r\n        header(\"Content-Type: text\/calendar\");\r\n        header(\"Content-Disposition: inline; filename=\" . $filename . '.ics');\r\n\r\n        $this->get_ical()->export($events, '', true, $attachments ? [$this->driver, 'get_attachment_body'] : null);\r\n\r\n        if ($terminate) {\r\n            exit;\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Handler for iCal feed requests\r\n     *\/\r\n    public function ical_feed_export()\r\n    {\r\n        $session_exists = !empty($_SESSION['user_id']);\r\n\r\n        \/\/ process HTTP auth info\r\n        if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {\r\n            $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; \/\/ used for rcmail::autoselect_host()\r\n            $auth = $this->rc->plugins->exec_hook('authenticate', [\r\n                'host' => $this->rc->autoselect_host(),\r\n                'user' => trim($_SERVER['PHP_AUTH_USER']),\r\n                'pass' => $_SERVER['PHP_AUTH_PW'],\r\n                'cookiecheck' => true,\r\n                'valid'       => true,\r\n            ]);\r\n\r\n            if ($auth['valid'] && !$auth['abort']) {\r\n                $this->rc->login($auth['user'], $auth['pass'], $auth['host']);\r\n            }\r\n        }\r\n\r\n        \/\/ require HTTP auth\r\n        if (empty($_SESSION['user_id'])) {\r\n            header('WWW-Authenticate: Basic realm=\"Kolab Calendar\"');\r\n            header('HTTP\/1.0 401 Unauthorized');\r\n            exit;\r\n        }\r\n\r\n        \/\/ decode calendar feed hash\r\n        $format  = 'ics';\r\n        $calhash = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GET);\r\n\r\n        if (preg_match(($suff_regex = '\/\\.([a-z0-9]{3,5})$\/i'), $calhash, $m)) {\r\n            $format  = strtolower($m[1]);\r\n            $calhash = preg_replace($suff_regex, '', $calhash);\r\n        }\r\n\r\n        if (!strpos($calhash, ':')) {\r\n            $calhash = base64_decode($calhash);\r\n        }\r\n\r\n        [$user, $_GET['source']] = explode(':', $calhash, 2);\r\n\r\n        \/\/ sanity check user\r\n        if ($this->rc->user->get_username() == $user) {\r\n            $this->setup();\r\n            $this->load_driver();\r\n            $this->export_events(false);\r\n        } else {\r\n            header('HTTP\/1.0 404 Not Found');\r\n        }\r\n\r\n        \/\/ don't save session data\r\n        if (!$session_exists) {\r\n            session_destroy();\r\n        }\r\n\r\n        exit;\r\n    }\r\n\r\n    \/**\r\n     *\r\n     *\/\r\n    public function load_settings()\r\n    {\r\n        $this->lib->load_settings();\r\n        $this->defaults += $this->lib->defaults;\r\n\r\n        $settings = [];\r\n\r\n        \/\/ configuration\r\n        $settings['default_view']     = (string) $this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);\r\n        $settings['timeslots']        = (int) $this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);\r\n        $settings['first_day']        = (int) $this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);\r\n        $settings['first_hour']       = (int) $this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);\r\n        $settings['work_start']       = (int) $this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);\r\n        $settings['work_end']         = (int) $this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);\r\n        $settings['agenda_range']     = (int) $this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']);\r\n        $settings['event_coloring']   = (int) $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);\r\n        $settings['time_indicator']   = (int) $this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);\r\n        $settings['invite_shared']    = (int) $this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);\r\n        $settings['itip_notify']      = (int) $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);\r\n        $settings['show_weekno']      = (int) $this->rc->config->get('calendar_show_weekno', $this->defaults['calendar_show_weekno']);\r\n        $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');\r\n        $settings['invitation_calendars'] = (bool) $this->rc->config->get('kolab_invitation_calendars', false);\r\n\r\n        \/\/ 'table' view has been replaced by 'list' view\r\n        if ($settings['default_view'] == 'table') {\r\n            $settings['default_view'] = 'list';\r\n        }\r\n\r\n        \/\/ get user identity to create default attendee\r\n        if ($this->ui->screen == 'calendar') {\r\n            foreach ($this->rc->user->list_emails() as $rec) {\r\n                if (empty($identity)) {\r\n                    $identity = $rec;\r\n                }\r\n\r\n                $identity['emails'][] = $rec['email'];\r\n                $settings['identities'][$rec['identity_id']] = $rec['email'];\r\n            }\r\n\r\n            $identity['emails'][] = $this->rc->user->get_username();\r\n            $identity['ownedResources'] = $this->owned_resources_emails();\r\n            $settings['identity'] = [\r\n                'name'   => $identity['name'],\r\n                'email'  => strtolower($identity['email']),\r\n                'emails' => ';' . strtolower(implode(';', $identity['emails'])),\r\n                'ownedResources' => ';' . strtolower(implode(';', $identity['ownedResources'])),\r\n            ];\r\n        }\r\n\r\n        \/\/ freebusy token authentication URL\r\n        if (($url = $this->rc->config->get('calendar_freebusy_session_auth_url'))\r\n            && ($uniqueid = $this->rc->config->get('kolab_uniqueid'))\r\n        ) {\r\n            if ($url === true) {\r\n                $url = '\/freebusy';\r\n            }\r\n            $url = rtrim(rcube_utils::resolve_url($url), '\/ ');\r\n            $url .= '\/' . urlencode($this->rc->get_user_name());\r\n            $url .= '\/' . urlencode($uniqueid);\r\n\r\n            $settings['freebusy_url'] = $url;\r\n        }\r\n\r\n        return $settings;\r\n    }\r\n\r\n    \/**\r\n     * Encode events as JSON\r\n     *\r\n     * @param array $events Events as array\r\n     * @param bool  $addcss Add CSS class names according to calendar and categories\r\n     *\r\n     * @return string JSON encoded events\r\n     *\/\r\n    public function encode($events, $addcss = false)\r\n    {\r\n        $json = [];\r\n        foreach ($events as $event) {\r\n            $json[] = $this->_client_event($event, $addcss);\r\n        }\r\n        return rcube_output::json_serialize($json);\r\n    }\r\n\r\n    \/**\r\n     * Convert an event object to be used on the client\r\n     *\/\r\n    private function _client_event($event, $addcss = false)\r\n    {\r\n        \/\/ compose a human readable strings for alarms_text and recurrence_text\r\n        if (!empty($event['valarms'])) {\r\n            $event['alarms_text'] = libcalendaring::alarms_text($event['valarms']);\r\n            $event['valarms'] = libcalendaring::to_client_alarms($event['valarms']);\r\n        }\r\n\r\n        if (!empty($event['recurrence'])) {\r\n            $event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);\r\n            $event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);\r\n            unset($event['recurrence_date']);\r\n        }\r\n\r\n        if (!empty($event['attachments'])) {\r\n            foreach ($event['attachments'] as $k => $attachment) {\r\n                $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);\r\n\r\n                unset($event['attachments'][$k]['data'], $event['attachments'][$k]['content']);\r\n\r\n                if (empty($attachment['id'])) {\r\n                    $event['attachments'][$k]['id'] = $k;\r\n                }\r\n            }\r\n        }\r\n\r\n        \/\/ convert link URIs references into structs\r\n        if (array_key_exists('links', $event)) {\r\n            foreach ((array) $event['links'] as $i => $link) {\r\n                if (strpos($link, 'imap:\/\/') === 0 && ($msgref = $this->driver->get_message_reference($link))) {\r\n                    $event['links'][$i] = $msgref;\r\n                }\r\n            }\r\n        }\r\n\r\n        \/\/ check for organizer in attendees list\r\n        $organizer = null;\r\n        if (!empty($event['attendees'])) {\r\n            foreach ((array) $event['attendees'] as $i => $attendee) {\r\n                if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER') {\r\n                    $organizer = $attendee;\r\n                }\r\n                if (!empty($attendee['status']) && $attendee['status'] == 'DELEGATED' && empty($attendee['rsvp'])) {\r\n                    $event['attendees'][$i]['noreply'] = true;\r\n                } else {\r\n                    unset($event['attendees'][$i]['noreply']);\r\n                }\r\n            }\r\n        }\r\n\r\n        if ($organizer === null && !empty($event['organizer'])) {\r\n            $organizer = $event['organizer'];\r\n            $organizer['role'] = 'ORGANIZER';\r\n            if (!isset($event['attendees']) || !is_array($event['attendees'])) {\r\n                $event['attendees'] = [$organizer];\r\n            }\r\n        }\r\n\r\n        \/\/ Convert HTML description into plain text\r\n        if ($this->is_html($event)) {\r\n            $h2t = new rcube_html2text($event['description'], false, true, 0);\r\n            $event['description'] = trim($h2t->get_text());\r\n        }\r\n\r\n        \/\/ mapping url => vurl, allday => allDay because of the fullcalendar client script\r\n        $event['vurl']   = $event['url'] ?? null;\r\n        $event['allDay'] = !empty($event['allday']);\r\n        unset($event['url']);\r\n        unset($event['allday']);\r\n\r\n        $event['className'] = !empty($event['className']) ? explode(' ', $event['className']) : [];\r\n\r\n        if ($event['allDay']) {\r\n            $event['end'] = $event['end']->add(new DateInterval('P1D'));\r\n        }\r\n\r\n        if (!empty($_GET['mode']) && $_GET['mode'] == 'print') {\r\n            $event['editable'] = false;\r\n        }\r\n\r\n        return [\r\n            '_id'     => $event['calendar'] . ':' . $event['id'],  \/\/ unique identifier for fullcalendar\r\n            'start'   => $this->lib->adjust_timezone($event['start'], $event['allDay'])->format('c'),\r\n            'end'     => $this->lib->adjust_timezone($event['end'], $event['allDay'])->format('c'),\r\n            \/\/ 'changed' might be empty for event recurrences (Bug #2185)\r\n            'changed' => !empty($event['changed']) ? $this->lib->adjust_timezone($event['changed'])->format('c') : null,\r\n            'created' => !empty($event['created']) ? $this->lib->adjust_timezone($event['created'])->format('c') : null,\r\n            'title'       => strval($event['title'] ?? null),\r\n            'description' => strval($event['description'] ?? null),\r\n            'location'    => strval($event['location'] ?? null),\r\n        ] + $event;\r\n    }\r\n\r\n    \/**\r\n     * Generate a unique identifier for an event\r\n     *\/\r\n    public function generate_uid()\r\n    {\r\n        return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));\r\n    }\r\n\r\n    \/**\r\n     * TEMPORARY: generate random event data for testing\r\n     * Create events by opening http:\/\/<roundcubeurl>\/?_task=calendar&_action=randomdata&_num=500&_date=2014-08-01&_dev=120\r\n     *\/\r\n    public function generate_randomdata()\r\n    {\r\n        @set_time_limit(0);\r\n\r\n        $num   = !empty($_REQUEST['_num']) ? intval($_REQUEST['_num']) : 100;\r\n        $date  = !empty($_REQUEST['_date']) ? $_REQUEST['_date'] : 'now';\r\n        $dev   = !empty($_REQUEST['_dev']) ? $_REQUEST['_dev'] : 30;\r\n        $cats  = array_keys($this->driver->list_categories());\r\n        $cals  = $this->driver->list_calendars(calendar_driver::FILTER_ACTIVE);\r\n        $count = 0;\r\n\r\n        while ($count++ < $num) {\r\n            $spread   = intval($dev) * 86400; \/\/ days\r\n            $refdate  = strtotime($date);\r\n            $start    = round(($refdate + rand(-$spread, $spread)) \/ 600) * 600;\r\n            $duration = round(rand(30, 360) \/ 30) * 30 * 60;\r\n            $allday   = rand(0, 20) > 18;\r\n            $alarm    = rand(-30, 12) * 5;\r\n            $fb       = rand(0, 2);\r\n\r\n            if (date('G', $start) > 23) {\r\n                $start -= 3600;\r\n            }\r\n\r\n            if ($allday) {\r\n                $start    = strtotime(date('Y-m-d 00:00:00', $start));\r\n                $duration = 86399;\r\n            }\r\n\r\n            $title = '';\r\n            $len = rand(2, 12);\r\n            $words = explode(\" \", \"The Hough transform is named after Paul Hough who patented the method in 1962.\"\r\n                . \" It is a technique which can be used to isolate features of a particular shape within an image.\"\r\n                . \" Because it requires that the desired features be specified in some parametric form, the classical\"\r\n                . \" Hough transform is most commonly used for the de- tection of regular curves such as lines, circles,\"\r\n                . \" ellipses, etc. A generalized Hough transform can be employed in applications where a simple\"\r\n                . \" analytic description of a feature(s) is not possible. Due to the computational complexity of\"\r\n                . \" the generalized Hough algorithm, we restrict the main focus of this discussion to the classical\"\r\n                . \" Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter\"\r\n                . \" referred to without the classical prefix ) retains many applications, as most manufac- tured\"\r\n                . \" parts (and many anatomical parts investigated in medical imagery) contain feature boundaries\"\r\n                . \" which can be described by regular curves. The main advantage of the Hough transform technique\"\r\n                . \" is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected\"\r\n                . \" by image noise.\");\r\n            \/\/ $chars = \"!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890\";\r\n            for ($i = 0; $i < $len; $i++) {\r\n                $title .= $words[rand(0, count($words) - 1)] . \" \";\r\n            }\r\n\r\n            $this->driver->new_event([\r\n                'uid'        => $this->generate_uid(),\r\n                'start'      => new DateTime('@' . $start),\r\n                'end'        => new DateTime('@' . ($start + $duration)),\r\n                'allday'     => $allday,\r\n                'title'      => rtrim($title),\r\n                'free_busy'  => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'),\r\n                'categories' => $cats[array_rand($cats)],\r\n                'calendar'   => array_rand($cals),\r\n                'alarms'     => $alarm > 0 ? \"-{$alarm}M:DISPLAY\" : '',\r\n                'priority'   => rand(0, 9),\r\n            ]);\r\n        }\r\n\r\n        $this->rc->output->redirect('');\r\n    }\r\n\r\n    \/**\r\n     * Handler for attachments upload\r\n     *\/\r\n    public function attachment_upload()\r\n    {\r\n        $handler = new kolab_attachments_handler();\r\n        $handler->attachment_upload(self::SESSION_KEY, 'cal-');\r\n    }\r\n\r\n    \/**\r\n     * Handler for attachments download\/displaying\r\n     *\/\r\n    public function attachment_get()\r\n    {\r\n        $handler = new kolab_attachments_handler();\r\n\r\n        \/\/ show loading page\r\n        if (!empty($_GET['_preload'])) {\r\n            return $handler->attachment_loading_page();\r\n        }\r\n\r\n        $event_id = rcube_utils::get_input_value('_event', rcube_utils::INPUT_GPC);\r\n        $calendar = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GPC);\r\n        $id       = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);\r\n        $rev      = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);\r\n\r\n        $event = ['id' => $event_id, 'calendar' => $calendar, 'rev' => $rev];\r\n\r\n        if ($calendar == '--invitation--itip') {\r\n            $uid  = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GPC);\r\n            $part = rcube_utils::get_input_value('_part', rcube_utils::INPUT_GPC);\r\n            $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC);\r\n\r\n            $event      = $this->lib->mail_get_itip_object($mbox, $uid, $part, 'event');\r\n            $attachment = $event['attachments'][$id];\r\n            $attachment['body'] = &$attachment['data'];\r\n        } else {\r\n            $attachment = $this->driver->get_attachment($id, $event);\r\n        }\r\n\r\n        \/\/ show part page\r\n        if (!empty($_GET['_frame'])) {\r\n            $handler->attachment_page($attachment);\r\n        }\r\n        \/\/ deliver attachment content\r\n        elseif ($attachment) {\r\n            if ($calendar != '--invitation--itip') {\r\n                $attachment['body'] = $this->driver->get_attachment_body($id, $event);\r\n            }\r\n\r\n            $handler->attachment_get($attachment);\r\n        }\r\n\r\n        \/\/ if we arrive here, the requested part was not found\r\n        header('HTTP\/1.1 404 Not Found');\r\n        exit;\r\n    }\r\n\r\n    \/**\r\n     * Determine whether the given event description is HTML formatted\r\n     *\/\r\n    private function is_html($event)\r\n    {\r\n        \/\/ check for opening and closing <html> or <body> tags\r\n        return !empty($event['description'])\r\n            && preg_match('\/<(html|body)(\\s+[a-z]|>)\/', $event['description'], $m)\r\n            && strpos($event['description'], '<\/'>') > 0;\r\n    }\r\n\r\n    \/**\r\n     * Prepares new\/edited event properties before save\r\n     *\/\r\n    private function write_preprocess(&$event, $action)\r\n    {\r\n        \/\/ Remove double timezone specification (T2313)\r\n        $event['start'] = preg_replace('\/\\s*\\(.*\\)\/', '', $event['start'] ?? '');\r\n        $event['end']   = preg_replace('\/\\s*\\(.*\\)\/', '', $event['end'] ?? '');\r\n\r\n        \/\/ convert dates into DateTime objects in user's current timezone\r\n        $event['start']  = new DateTime($event['start'], $this->timezone);\r\n        $event['end']    = new DateTime($event['end'], $this->timezone);\r\n        $event['allday'] = !empty($event['allDay']);\r\n        unset($event['allDay']);\r\n\r\n        \/\/ start\/end is all we need for 'move' action (#1480)\r\n        if ($action == 'move') {\r\n            return true;\r\n        }\r\n\r\n        \/\/ convert the submitted recurrence settings\r\n        if (!empty($event['recurrence'])) {\r\n            $event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']);\r\n\r\n            \/\/ align start date with the first occurrence\r\n            if (!empty($event['recurrence']) && !empty($event['syncstart'])\r\n                && (empty($event['_savemode']) || $event['_savemode'] == 'all')\r\n            ) {\r\n                $next = $this->find_first_occurrence($event);\r\n\r\n                if (!$next) {\r\n                    $this->rc->output->show_message('calendar.recurrenceerror', 'error');\r\n                    return false;\r\n                } elseif ($event['start'] != $next) {\r\n                    $diff = $event['start']->diff($event['end'], true);\r\n\r\n                    $event['start'] = $next;\r\n                    $event['end']   = clone $next;\r\n                    $event['end']->add($diff);\r\n                }\r\n            }\r\n        }\r\n\r\n        \/\/ convert the submitted alarm values\r\n        if (!empty($event['valarms'])) {\r\n            $event['valarms'] = libcalendaring::from_client_alarms($event['valarms']);\r\n        }\r\n\r\n        $eventid = 'cal-' . (!empty($event['id']) ? $event['id'] : 'new');\r\n        $handler = new kolab_attachments_handler();\r\n        $event['attachments'] = $handler->attachments_set(self::SESSION_KEY, $eventid, $event['attachments'] ?? []);\r\n\r\n        \/\/ convert link references into simple URIs\r\n        if (array_key_exists('links', $event)) {\r\n            $event['links'] = array_map(\r\n                function ($link) {\r\n                    return is_array($link) ? $link['uri'] : strval($link);\r\n                },\r\n                (array) $event['links']\r\n            );\r\n        }\r\n\r\n        \/\/ check for organizer in attendees\r\n        if ($action == 'new' || $action == 'edit') {\r\n            if (empty($event['attendees'])) {\r\n                $event['attendees'] = [];\r\n            }\r\n\r\n            $emails = $this->get_user_emails();\r\n            $organizer = $owner = false;\r\n\r\n            foreach ((array) $event['attendees'] as $i => $attendee) {\r\n                if (($attendee['role'] ?? '') == 'ORGANIZER') {\r\n                    $organizer = $i;\r\n                }\r\n                if (!empty($attendee['email']) && in_array(strtolower($attendee['email']), $emails)) {\r\n                    $owner = $i;\r\n                }\r\n                if (!isset($attendee['rsvp'])) {\r\n                    $event['attendees'][$i]['rsvp'] = true;\r\n                } elseif (is_string($attendee['rsvp'])) {\r\n                    $event['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';\r\n                }\r\n            }\r\n\r\n            if (!empty($event['_identity'])) {\r\n                $identity = $this->rc->user->get_identity($event['_identity']);\r\n            }\r\n\r\n            \/\/ set new organizer identity\r\n            if ($organizer !== false && !empty($identity)) {\r\n                $event['attendees'][$organizer]['name']  = $identity['name'];\r\n                $event['attendees'][$organizer]['email'] = $identity['email'];\r\n            }\r\n            \/\/ set owner as organizer if yet missing\r\n            elseif ($organizer === false && $owner !== false) {\r\n                $event['attendees'][$owner]['role'] = 'ORGANIZER';\r\n                unset($event['attendees'][$owner]['rsvp']);\r\n            }\r\n            \/\/ fallback to the selected identity\r\n            elseif ($organizer === false && !empty($identity)) {\r\n                $event['attendees'][] = [\r\n                    'role'  => 'ORGANIZER',\r\n                    'name'  => $identity['name'],\r\n                    'email' => $identity['email'],\r\n                ];\r\n            }\r\n        }\r\n\r\n        \/\/ mapping url => vurl because of the fullcalendar client script\r\n        if (array_key_exists('vurl', $event)) {\r\n            $event['url'] = $event['vurl'];\r\n            unset($event['vurl']);\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    \/**\r\n     * Releases some resources after successful event save\r\n     *\/\r\n    private function cleanup_event(&$event)\r\n    {\r\n        $handler = new kolab_attachments_handler();\r\n        $handler->attachments_cleanup(self::SESSION_KEY);\r\n    }\r\n\r\n    \/**\r\n     * Send out an invitation\/notification to all event attendees\r\n     *\/\r\n    private function notify_attendees($event, $old, $action = 'edit', $comment = null, $rsvp = null)\r\n    {\r\n        $is_cancelled = $action == 'remove'\r\n            || (!empty($event['status']) && $event['status'] == 'CANCELLED' && ($old['status'] ?? '') != $event['status']);\r\n\r\n        $event['cancelled'] = $is_cancelled;\r\n\r\n        if ($rsvp === null) {\r\n            $rsvp = !$old || ($event['sequence'] ?? 0) > ($old['sequence'] ?? 0);\r\n        }\r\n\r\n        $itip        = $this->load_itip();\r\n        $emails      = $this->get_user_emails();\r\n        $itip_notify = (int) $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);\r\n\r\n        \/\/ add comment to the iTip attachment\r\n        $event['comment'] = $comment;\r\n\r\n        \/\/ set a valid recurrence-id if this is a recurrence instance\r\n        libcalendaring::identify_recurrence_instance($event);\r\n\r\n        \/\/ compose multipart message using PEAR:Mail_Mime\r\n        $method  = $action == 'remove' ? 'CANCEL' : 'REQUEST';\r\n        $message = $itip->compose_itip_message($event, $method, $rsvp);\r\n\r\n        \/\/ list existing attendees from $old event\r\n        $old_attendees = [];\r\n        if (!empty($old['attendees'])) {\r\n            foreach ((array) $old['attendees'] as $attendee) {\r\n                $old_attendees[] = $attendee['email'];\r\n            }\r\n        }\r\n\r\n        \/\/ send to every attendee\r\n        $sent    = 0;\r\n        $current = [];\r\n        foreach ((array) $event['attendees'] as $attendee) {\r\n            \/\/ skip myself for obvious reasons\r\n            if (empty($attendee['email']) || in_array(strtolower($attendee['email']), $emails)) {\r\n                continue;\r\n            }\r\n\r\n            $current[] = strtolower($attendee['email']);\r\n\r\n            \/\/ skip if notification is disabled for this attendee\r\n            if (!empty($attendee['noreply']) && $itip_notify & 2) {\r\n                continue;\r\n            }\r\n\r\n            \/\/ skip if this attendee has delegated and set RSVP=FALSE\r\n            if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) {\r\n                continue;\r\n            }\r\n\r\n            \/\/ which template to use for mail text\r\n            $is_new   = !in_array($attendee['email'], $old_attendees);\r\n            $is_rsvp  = $is_new || $event['sequence'] > $old['sequence'];\r\n            $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');\r\n            $subject  = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject' : 'eventupdatesubjectempty'));\r\n\r\n            $event['comment'] = $comment;\r\n\r\n            \/\/ finally send the message\r\n            if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message, $is_rsvp)) {\r\n                $sent++;\r\n            } else {\r\n                $sent = -100;\r\n            }\r\n        }\r\n\r\n        \/\/ TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions\r\n\r\n        \/\/ send CANCEL message to removed attendees\r\n        if (!empty($old['attendees'])) {\r\n            foreach ($old['attendees'] as $attendee) {\r\n                if ($attendee['role'] == 'ORGANIZER'\r\n                    || empty($attendee['email'])\r\n                    || in_array(strtolower($attendee['email']), $current)\r\n                ) {\r\n                    continue;\r\n                }\r\n\r\n                $vevent = $old;\r\n                $vevent['cancelled'] = $is_cancelled;\r\n                $vevent['attendees'] = [$attendee];\r\n                $vevent['comment']   = $comment;\r\n\r\n                if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody')) {\r\n                    $sent++;\r\n                } else {\r\n                    $sent = -100;\r\n                }\r\n            }\r\n        }\r\n\r\n        return $sent;\r\n    }\r\n\r\n    \/**\r\n     * Echo simple free\/busy status text for the given user and time range\r\n     *\/\r\n    public function freebusy_status()\r\n    {\r\n        $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);\r\n        $start = $this->input_timestamp('start', rcube_utils::INPUT_GPC);\r\n        $end   = $this->input_timestamp('end', rcube_utils::INPUT_GPC);\r\n\r\n        if (!$start) {\r\n            $start = time();\r\n        }\r\n        if (!$end) {\r\n            $end = $start + 3600;\r\n        }\r\n\r\n        $status = 'UNKNOWN';\r\n        $fbtypemap = [\r\n            calendar::FREEBUSY_UNKNOWN   => 'UNKNOWN',\r\n            calendar::FREEBUSY_FREE      => 'FREE',\r\n            calendar::FREEBUSY_BUSY      => 'BUSY',\r\n            calendar::FREEBUSY_TENTATIVE => 'TENTATIVE',\r\n            calendar::FREEBUSY_OOF       => 'OUT-OF-OFFICE',\r\n        ];\r\n\r\n        \/\/ if the backend has free-busy information\r\n        $fblist = $this->driver->get_freebusy_list($email, $start, $end);\r\n\r\n        if (is_array($fblist)) {\r\n            $status = 'FREE';\r\n\r\n            foreach ($fblist as $slot) {\r\n                [$from, $to, $type] = $slot;\r\n                if ($from < $end && $to > $start) {\r\n                    $status = isset($type) && !empty($fbtypemap[$type]) ? $fbtypemap[$type] : 'BUSY';\r\n                    break;\r\n                }\r\n            }\r\n        }\r\n\r\n        \/\/ let this information be cached for 5min\r\n        $this->rc->output->future_expire_header(300);\r\n\r\n        echo $status;\r\n        exit;\r\n    }\r\n\r\n    \/**\r\n     * Return a list of free\/busy time slots within the given period\r\n     * Echo data in JSON encoding\r\n     *\/\r\n    public function freebusy_times()\r\n    {\r\n        $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);\r\n        $start = $this->input_timestamp('start', rcube_utils::INPUT_GPC);\r\n        $end   = $this->input_timestamp('end', rcube_utils::INPUT_GPC);\r\n        $interval  = intval(rcube_utils::get_input_value('interval', rcube_utils::INPUT_GPC));\r\n        $strformat = $interval > 60 ? 'Ymd' : 'YmdHis';\r\n\r\n        if (!$start) {\r\n            $start = time();\r\n        }\r\n        if (!$end) {\r\n            $end = $start + 86400 * 30;\r\n        }\r\n        if (!$interval) {\r\n            $interval = 60;\r\n        }  \/\/ 1 hour\r\n\r\n        $fblist = $this->driver->get_freebusy_list($email, $start, $end);\r\n        $slots  = '';\r\n\r\n        \/\/ prepare freebusy list before use (for better performance)\r\n        if (is_array($fblist)) {\r\n            foreach ($fblist as $idx => $slot) {\r\n                [$from, $to, ] = $slot;\r\n\r\n                \/\/ check for possible all-day times\r\n                if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') {\r\n                    \/\/ shift into the user's timezone for sane matching\r\n                    $fblist[$idx][0] -= $this->gmt_offset;\r\n                    $fblist[$idx][1] -= $this->gmt_offset;\r\n                }\r\n            }\r\n        }\r\n\r\n        \/\/ build a list from $start till $end with blocks representing the fb-status\r\n        $t_end = 0;\r\n        for ($s = 0, $t = $start; $t <= $end; $s++) {\r\n            $t_end = $t + $interval * 60;\r\n            $dt = new DateTime('@' . $t);\r\n            $dt->setTimezone($this->timezone);\r\n\r\n            \/\/ determine attendee's status\r\n            if (is_array($fblist)) {\r\n                $status = self::FREEBUSY_FREE;\r\n\r\n                foreach ($fblist as $slot) {\r\n                    [$from, $to, $type] = $slot;\r\n\r\n                    if ($from < $t_end && $to > $t) {\r\n                        $status = $type ?? self::FREEBUSY_BUSY;\r\n                        if ($status == self::FREEBUSY_BUSY) {\r\n                            \/\/ can't get any worse \ud83d\ude42\r\n                            break;\r\n                        }\r\n                    }\r\n                }\r\n            } else {\r\n                $status = self::FREEBUSY_UNKNOWN;\r\n            }\r\n\r\n            \/\/ use most compact format, assume $status is one digit\/character\r\n            $slots .= $status;\r\n            $t = $t_end;\r\n        }\r\n\r\n        $dts = new DateTime('@' . $start);\r\n        $dts->setTimezone($this->timezone);\r\n        $dte = new DateTime('@' . $t_end);\r\n        $dte->setTimezone($this->timezone);\r\n\r\n        \/\/ let this information be cached for 5min\r\n        $this->rc->output->future_expire_header(300);\r\n\r\n        echo rcube_output::json_serialize([\r\n            'email' => $email,\r\n            'start' => $dts->format('c'),\r\n            'end'   => $dte->format('c'),\r\n            'interval' => $interval,\r\n            'slots' => $slots,\r\n        ]);\r\n        exit;\r\n    }\r\n\r\n    \/**\r\n     * Handler for printing calendars\r\n     *\/\r\n    public function print_view()\r\n    {\r\n        $title = $this->gettext('print');\r\n\r\n        $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC);\r\n        if (!in_array($view, ['agendaWeek', 'agendaDay', 'month', 'list'])) {\r\n            $view = 'agendaDay';\r\n        }\r\n\r\n        $this->rc->output->set_env('view', $view);\r\n\r\n        if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC)) {\r\n            $this->rc->output->set_env('date', $date);\r\n        }\r\n\r\n        if ($range = rcube_utils::get_input_value('range', rcube_utils::INPUT_GPC)) {\r\n            $this->rc->output->set_env('listRange', intval($range));\r\n        }\r\n\r\n        if ($search = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC)) {\r\n            $this->rc->output->set_env('search', $search);\r\n            $title .= ' \"' . $search . '\"';\r\n        }\r\n\r\n        \/\/ Add JS to the page\r\n        $this->ui->addJS();\r\n\r\n        $this->register_handler('plugin.calendar_css', [$this->ui, 'calendar_css']);\r\n        $this->register_handler('plugin.calendar_list', [$this->ui, 'calendar_list']);\r\n\r\n        $this->rc->output->set_pagetitle($title);\r\n        $this->rc->output->send('calendar.print');\r\n    }\r\n\r\n    \/**\r\n     * Compare two event objects and return differing properties\r\n     *\r\n     * @param array $a Event A\r\n     * @param array $b Event B\r\n     *\r\n     * @return array List of differing event properties\r\n     *\/\r\n    public static function event_diff($a, $b)\r\n    {\r\n        $diff   = [];\r\n        $ignore = ['changed' => 1, 'attachments' => 1];\r\n\r\n        foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {\r\n            if (empty($ignore[$key]) && $key[0] != '_') {\r\n                $av = $a[$key] ?? null;\r\n                $bv = $b[$key] ?? null;\r\n\r\n                if ($av != $bv) {\r\n                    $diff[] = $key;\r\n                }\r\n            }\r\n        }\r\n\r\n        \/\/ only compare number of attachments\r\n        $ac = !empty($a['attachments']) ? count($a['attachments']) : 0;\r\n        $bc = !empty($b['attachments']) ? count($b['attachments']) : 0;\r\n\r\n        if ($ac != $bc) {\r\n            $diff[] = 'attachments';\r\n        }\r\n\r\n        return $diff;\r\n    }\r\n\r\n    \/**\r\n     * Update attendee properties on the given event object\r\n     *\r\n     * @param array $event     The event object to be altered\r\n     * @param array $attendees List of hash arrays each represeting an updated\/added attendee\r\n     * @param array $removed   List of attendees' addresses to remove\r\n     *\/\r\n    public static function merge_attendee_data(&$event, $attendees, $removed = [])\r\n    {\r\n        if (!empty($attendees) && !is_array($attendees[0])) {\r\n            $attendees = [$attendees];\r\n        }\r\n\r\n        foreach ($attendees as $attendee) {\r\n            $found = false;\r\n\r\n            foreach ($event['attendees'] as $i => $candidate) {\r\n                if ($candidate['email'] == $attendee['email']) {\r\n                    $event['attendees'][$i] = $attendee;\r\n                    $found = true;\r\n                    break;\r\n                }\r\n            }\r\n\r\n            if (!$found) {\r\n                $event['attendees'][] = $attendee;\r\n            }\r\n        }\r\n\r\n        \/\/ filter out removed attendees\r\n        if (!empty($removed)) {\r\n            $event['attendees'] = array_filter($event['attendees'], function ($attendee) use ($removed) {\r\n                return !in_array($attendee['email'], $removed);\r\n            });\r\n        }\r\n    }\r\n\r\n    \/****  Resource management functions  ****\/\r\n\r\n    \/**\r\n     * Getter for the configured implementation of the resource directory interface\r\n     *\/\r\n    private function resources_directory()\r\n    {\r\n        if (!empty($this->resources_dir)) {\r\n            return $this->resources_dir;\r\n        }\r\n\r\n        if ($driver_name = $this->rc->config->get('calendar_resources_driver')) {\r\n            $driver_class = 'resources_driver_' . $driver_name;\r\n\r\n            require_once $this->home . '\/drivers\/resources_driver.php';\r\n            require_once $this->home . '\/drivers\/' . $driver_name . '\/' . $driver_class . '.php';\r\n\r\n            $this->resources_dir = new $driver_class($this);\r\n        }\r\n\r\n        return $this->resources_dir;\r\n    }\r\n\r\n    \/**\r\n     * Handler for resoruce autocompletion requests\r\n     *\/\r\n    public function resources_autocomplete()\r\n    {\r\n        $search  = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);\r\n        $sid     = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);\r\n        $maxnum  = (int)$this->rc->config->get('autocomplete_max', 15);\r\n        $results = [];\r\n\r\n        if ($directory = $this->resources_directory()) {\r\n            foreach ($directory->load_resources($search, $maxnum) as $rec) {\r\n                $results[]  = [\r\n                    'name'  => $rec['name'],\r\n                    'email' => $rec['email'],\r\n                    'type'  => $rec['_type'],\r\n                ];\r\n            }\r\n        }\r\n\r\n        $this->rc->output->command('ksearch_query_results', $results, $search, $sid);\r\n        $this->rc->output->send();\r\n    }\r\n\r\n    \/**\r\n     * Handler for load-requests for resource data\r\n     *\/\r\n    public function resources_list()\r\n    {\r\n        $data = [];\r\n\r\n        if ($directory = $this->resources_directory()) {\r\n            foreach ($directory->load_resources() as $rec) {\r\n                $data[] = $rec;\r\n            }\r\n        }\r\n\r\n        $this->rc->output->command('plugin.resource_data', $data);\r\n        $this->rc->output->send();\r\n    }\r\n\r\n    \/**\r\n     * Handler for requests loading resource owner information\r\n     *\/\r\n    public function resources_owner()\r\n    {\r\n        if ($directory = $this->resources_directory()) {\r\n            $id   = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);\r\n            $data = $directory->get_resource_owner($id);\r\n        }\r\n\r\n        $this->rc->output->command('plugin.resource_owner', $data ?? null);\r\n        $this->rc->output->send();\r\n    }\r\n\r\n    \/**\r\n     * Deliver event data for a resource's calendar\r\n     *\/\r\n    public function resources_calendar()\r\n    {\r\n        $events = [];\r\n\r\n        if ($directory = $this->resources_directory()) {\r\n            $id    = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);\r\n            $start = $this->input_timestamp('start', rcube_utils::INPUT_GET);\r\n            $end   = $this->input_timestamp('end', rcube_utils::INPUT_GET);\r\n\r\n            $events = $directory->get_resource_calendar($id, $start, $end);\r\n        }\r\n\r\n        echo $this->encode($events);\r\n        exit;\r\n    }\r\n\r\n    \/**\r\n     * List email addressed of owned resources\r\n     *\/\r\n    private function owned_resources_emails()\r\n    {\r\n        $results = [];\r\n        if ($directory = $this->resources_directory()) {\r\n            foreach ($directory->load_resources($_SESSION['kolab_dn'], 5000, 'owner') as $rec) {\r\n                $results[] = $rec['email'];\r\n            }\r\n        }\r\n        return $results;\r\n    }\r\n\r\n\r\n    \/****  Event invitation plugin hooks ****\/\r\n\r\n    \/**\r\n     * Find an event in user calendars\r\n     *\/\r\n    protected function find_event($event, &$mode)\r\n    {\r\n        $this->load_driver();\r\n\r\n        \/\/ We search for writeable calendars in personal namespace by default\r\n        $mode   = calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL;\r\n        $result = $this->driver->get_event($event, $mode);\r\n        \/\/ ... now check shared folders if not found\r\n        if (!$result) {\r\n            $result = $this->driver->get_event($event, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_SHARED);\r\n            if ($result) {\r\n                $mode |= calendar_driver::FILTER_SHARED;\r\n            }\r\n        }\r\n\r\n        return $result;\r\n    }\r\n\r\n    \/**\r\n     * Handler for calendar\/itip-status requests\r\n     *\/\r\n    public function event_itip_status()\r\n    {\r\n        $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);\r\n\r\n        $this->load_driver();\r\n\r\n        \/\/ find local copy of the referenced event (in personal namespace)\r\n        $existing  = $this->find_event($data, $mode);\r\n        $is_shared = $mode & calendar_driver::FILTER_SHARED;\r\n        $itip      = $this->load_itip();\r\n        $response  = $itip->get_itip_status($data, $existing);\r\n        $calendars = null;\r\n\r\n        \/\/ get a list of writeable calendars to save new events to\r\n        if (\r\n            (!$existing || $is_shared)\r\n            && empty($data['nosave'])\r\n            && ($response['action'] == 'rsvp' || $response['action'] == 'import')\r\n        ) {\r\n            $calendars       = $this->driver->list_calendars($mode);\r\n            $calendar_select = new html_select([\r\n                'name'       => 'calendar',\r\n                'id'         => 'itip-saveto',\r\n                'is_escaped' => true,\r\n                'class'      => 'form-control custom-select',\r\n            ]);\r\n\r\n            $calendar_select->add('--', '');\r\n            $numcals = 0;\r\n            foreach ($calendars as $calendar) {\r\n                if (!empty($calendar['editable'])) {\r\n                    $calendar_select->add($calendar['name'], $calendar['id']);\r\n                    $numcals++;\r\n                }\r\n            }\r\n            if ($numcals < 1) {\r\n                $calendar_select = null;\r\n            }\r\n        }\r\n\r\n        if (!empty($calendar_select)) {\r\n            $default_calendar   = $this->get_default_calendar($calendars);\r\n            $response['select'] = html::span(\r\n                'folder-select',\r\n                $this->gettext('saveincalendar')\r\n                . '&nbsp;'\r\n                . $calendar_select->show($is_shared ? $existing['calendar'] : $default_calendar['id'])\r\n            );\r\n        } elseif (!empty($data['nosave'])) {\r\n            $response['select'] = html::tag('input', ['type' => 'hidden', 'name' => 'calendar', 'id' => 'itip-saveto', 'value' => '']);\r\n        }\r\n\r\n        \/\/ render small agenda view for the respective day\r\n        if ($data['method'] == 'REQUEST' && !empty($data['date']) && $response['action'] == 'rsvp') {\r\n            $event_start = rcube_utils::anytodatetime($data['date']);\r\n            $day_start   = new Datetime(gmdate('Y-m-d 00:00', $data['date']), $this->lib->timezone);\r\n            $day_end     = new Datetime(gmdate('Y-m-d 23:59', $data['date']), $this->lib->timezone);\r\n\r\n            \/\/ get events on that day from the user's personal calendars\r\n            $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);\r\n            $events    = $this->driver->load_events($day_start->format('U'), $day_end->format('U'), null, array_keys($calendars));\r\n\r\n            usort($events, function ($a, $b) { return $a['start'] > $b['start'] ? 1 : -1; });\r\n\r\n            $before = $after = [];\r\n            foreach ($events as $event) {\r\n                \/\/ TODO: skip events with free_busy == 'free' ?\r\n                if ($event['uid'] == $data['uid']\r\n                    || $event['end'] < $day_start || $event['start'] > $day_end\r\n                    || (!empty($event['status']) && $event['status'] == 'CANCELLED')\r\n                    || (!empty($event['className']) && strpos($event['className'], 'declined') !== false)\r\n                ) {\r\n                    continue;\r\n                }\r\n\r\n                if ($event['start'] < $event_start) {\r\n                    $before[] = $this->mail_agenda_event_row($event);\r\n                } else {\r\n                    $after[] = $this->mail_agenda_event_row($event);\r\n                }\r\n            }\r\n\r\n            $response['append'] = [\r\n                'selector' => '.calendar-agenda-preview',\r\n                'replacements' => [\r\n                    '%before%' => !empty($before) ? implode(\"\\n\", array_slice($before, -3)) : html::div('event-row no-event', $this->gettext('noearlierevents')),\r\n                    '%after%'  => !empty($after) ? implode(\"\\n\", array_slice($after, 0, 3)) : html::div('event-row no-event', $this->gettext('nolaterevents')),\r\n                ],\r\n            ];\r\n        }\r\n\r\n        $this->rc->output->command('plugin.update_itip_object_status', $response);\r\n    }\r\n\r\n    \/**\r\n     * Handler for calendar\/itip-remove requests\r\n     *\/\r\n    public function event_itip_remove()\r\n    {\r\n        $uid      = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);\r\n        $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);\r\n        $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);\r\n        $listmode = calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL;\r\n        $success  = false;\r\n\r\n        \/\/ search for event if only UID is given\r\n        if ($event = $this->driver->get_event(['uid' => $uid, '_instance' => $instance], $listmode)) {\r\n            $event['_savemode'] = $savemode;\r\n            $success = $this->driver->remove_event($event, true);\r\n        }\r\n\r\n        if ($success) {\r\n            $this->rc->output->show_message('calendar.successremoval', 'confirmation');\r\n        } else {\r\n            $this->rc->output->show_message('calendar.errorsaving', 'error');\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Handler for URLs that allow an invitee to respond on his invitation mail\r\n     *\/\r\n    public function itip_attend_response($p)\r\n    {\r\n        $this->setup();\r\n\r\n        if ($p['action'] == 'attend') {\r\n            $this->ui->init();\r\n\r\n            $this->rc->output->set_env('task', 'calendar');  \/\/ override some env vars\r\n            $this->rc->output->set_env('refresh_interval', 0);\r\n            $this->rc->output->set_pagetitle($this->gettext('calendar'));\r\n\r\n            $itip  = $this->load_itip();\r\n            $token = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);\r\n\r\n            \/\/ read event info stored under the given token\r\n            if ($invitation = $itip->get_invitation($token)) {\r\n                $this->token = $token;\r\n                $this->event = $invitation['event'];\r\n\r\n                \/\/ show message about cancellation\r\n                if (!empty($invitation['cancelled'])) {\r\n                    $this->invitestatus = html::div('rsvp-status declined', $itip->gettext('eventcancelled'));\r\n                }\r\n                \/\/ save submitted RSVP status\r\n                elseif (!empty($_POST['rsvp'])) {\r\n                    $status = null;\r\n                    foreach (['accepted', 'tentative', 'declined'] as $method) {\r\n                        if ($_POST['rsvp'] == $itip->gettext('itip' . $method)) {\r\n                            $status = $method;\r\n                            break;\r\n                        }\r\n                    }\r\n\r\n                    \/\/ send itip reply to organizer\r\n                    $invitation['event']['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);\r\n                    if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {\r\n                        $this->invitestatus = html::div('rsvp-status ' . strtolower($status), $itip->gettext('youhave' . strtolower($status)));\r\n                    } else {\r\n                        $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1);\r\n                    }\r\n\r\n                    \/\/ if user is logged in...\r\n                    \/\/ FIXME: we should really consider removing this functionality\r\n                    \/\/        it's confusing that it creates\/updates an event only for logged-in user\r\n                    \/\/        what if the logged-in user is not the same as the attendee?\r\n                    if ($this->rc->user->ID) {\r\n                        $this->load_driver();\r\n\r\n                        $invitation = $itip->get_invitation($token);\r\n                        $existing   = $this->driver->get_event($this->event);\r\n\r\n                        \/\/ save the event to his\/her default calendar if not yet present\r\n                        if (!$existing && ($calendar = $this->get_default_calendar())) {\r\n                            $invitation['event']['calendar'] = $calendar['id'];\r\n                            if ($this->driver->new_event($invitation['event'])) {\r\n                                $msg = $this->gettext(['name' => 'importedsuccessfully', 'vars' => ['calendar' => $calendar['name']]]);\r\n                                $this->rc->output->command('display_message', $msg, 'confirmation');\r\n                            } else {\r\n                                $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');\r\n                            }\r\n                        } elseif ($existing\r\n                            && ($this->event['sequence'] >= $existing['sequence']\r\n                                || $this->event['changed'] >= $existing['changed'])\r\n                            && ($calendar = $this->driver->get_calendar_name($existing['calendar']))\r\n                        ) {\r\n                            $this->event       = $invitation['event'];\r\n                            $this->event['id'] = $existing['id'];\r\n\r\n                            unset($this->event['comment']);\r\n\r\n                            \/\/ merge attendees status\r\n                            \/\/ e.g. preserve my participant status for regular updates\r\n                            $this->lib->merge_attendees($this->event, $existing, $status);\r\n\r\n                            \/\/ update attachments list\r\n                            $event['deleted_attachments'] = true;\r\n\r\n                            \/\/ show me as free when declined (#1670)\r\n                            if ($status == 'declined') {\r\n                                $this->event['free_busy'] = 'free';\r\n                            }\r\n\r\n                            if ($this->driver->edit_event($this->event)) {\r\n                                $msg = $this->gettext(['name' => 'updatedsuccessfully', 'vars' => ['calendar' => $calendar]]);\r\n                                $this->rc->output->command('display_message', $msg, 'confirmation');\r\n                            } else {\r\n                                $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n\r\n                $this->register_handler('plugin.event_inviteform', [$this, 'itip_event_inviteform']);\r\n                $this->register_handler('plugin.event_invitebox', [$this->ui, 'event_invitebox']);\r\n\r\n                if (empty($this->invitestatus)) {\r\n                    $this->itip->set_rsvp_actions(['accepted', 'tentative', 'declined']);\r\n                    $this->register_handler('plugin.event_rsvp_buttons', [$this->ui, 'event_rsvp_buttons']);\r\n                }\r\n\r\n                $this->rc->output->set_pagetitle($itip->gettext('itipinvitation') . ' ' . $this->event['title']);\r\n            } else {\r\n                $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);\r\n            }\r\n\r\n            $this->rc->output->send('calendar.itipattend');\r\n        }\r\n    }\r\n\r\n    \/**\r\n     *\r\n     *\/\r\n    public function itip_event_inviteform($attrib)\r\n    {\r\n        $hidden = new html_hiddenfield(['name' => \"_t\", 'value' => $this->token]);\r\n\r\n        return html::tag(\r\n            'form',\r\n            [\r\n                'action' => $this->rc->url(['task' => 'calendar', 'action' => 'attend']),\r\n                'method' => 'post',\r\n                'noclose' => true,\r\n            ] + $attrib\r\n        ) . $hidden->show();\r\n    }\r\n\r\n    \/**\r\n     *\r\n     *\/\r\n    private function mail_agenda_event_row($event, $class = '')\r\n    {\r\n        if (!empty($event['allday'])) {\r\n            $time = $this->gettext('all-day');\r\n        } else {\r\n            $start = is_object($event['start']) ? clone $event['start'] : $event['start'];\r\n            $end = is_object($event['end']) ? clone $event['end'] : $event['end'];\r\n\r\n            $time = $this->rc->format_date($start, $this->rc->config->get('time_format'))\r\n                . ' - ' . $this->rc->format_date($end, $this->rc->config->get('time_format'));\r\n        }\r\n\r\n        return html::div(\r\n            rtrim('event-row ' . ($class ?: ($event['className'] ?? ''))),\r\n            html::span('event-date', $time)\r\n            . html::span('event-title', rcube::Q($event['title']))\r\n        );\r\n    }\r\n\r\n    \/**\r\n     *\r\n     *\/\r\n    public function mail_messages_list($p)\r\n    {\r\n        if (!empty($p['cols']) && in_array('attachment', (array) $p['cols']) && !empty($p['messages'])) {\r\n            \/** @var rcube_message_header $header *\/\r\n            foreach ($p['messages'] as $header) {\r\n                $part = new StdClass();\r\n                $part->mimetype = $header->ctype;\r\n\r\n                if (libcalendaring::part_is_vcalendar($part)) {\r\n                    $header->list_flags['attachmentClass'] = 'ical';\r\n                } elseif (in_array($header->ctype, ['multipart\/alternative', 'multipart\/mixed'])) {\r\n                    \/\/ TODO: fetch bodystructure and search for ical parts. Maybe too expensive?\r\n                    if (!empty($header->structure) && !empty($header->structure->parts)) {\r\n                        foreach ($header->structure->parts as $part) {\r\n                            if (libcalendaring::part_is_vcalendar($part)\r\n                                && !empty($part->ctype_parameters['method'])\r\n                            ) {\r\n                                $header->list_flags['attachmentClass'] = 'ical';\r\n                                break;\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Add UI element to copy event invitations or updates to the calendar\r\n     *\/\r\n    public function mail_messagebody_html($p)\r\n    {\r\n        \/\/ load iCalendar functions (if necessary)\r\n        if (!empty($this->lib->ical_parts)) {\r\n            $this->get_ical();\r\n            $this->load_itip();\r\n        }\r\n\r\n        $html = '';\r\n        $has_events = false;\r\n        $ical_objects = $this->lib->get_mail_ical_objects();\r\n\r\n        \/\/ show a box for every event in the file\r\n        foreach ($ical_objects as $idx => $event) {\r\n            if ($event['_type'] != 'event') {\r\n                \/\/ skip non-event objects (#2928)\r\n                continue;\r\n            }\r\n\r\n            $has_events = true;\r\n\r\n            \/\/ get prepared inline UI for this event object\r\n            if ($ical_objects->method) {\r\n                $append   = '';\r\n                $date_str = $this->rc->format_date(clone $event['start'], $this->rc->config->get('date_format'), empty($event['start']->_dateonly));\r\n                $date     = new DateTime($event['start']->format('Y-m-d') . ' 12:00:00', new DateTimeZone('UTC'));\r\n\r\n                \/\/ prepare a small agenda preview to be filled with actual event data on async request\r\n                if ($ical_objects->method == 'REQUEST') {\r\n                    $append = html::div(\r\n                        'calendar-agenda-preview',\r\n                        html::tag('h3', 'preview-title', $this->gettext('agenda') . ' ' . html::span('date', $date_str))\r\n                        . '%before%' . $this->mail_agenda_event_row($event, 'current') . '%after%'\r\n                    );\r\n                }\r\n\r\n                $html .= html::div(\r\n                    'calendar-invitebox invitebox boxinformation',\r\n                    $this->itip->mail_itip_inline_ui(\r\n                        $event,\r\n                        $ical_objects->method,\r\n                        $ical_objects->mime_id . ':' . $idx,\r\n                        'calendar',\r\n                        rcube_utils::anytodatetime($ical_objects->message_date),\r\n                        $this->rc->url(['task' => 'calendar']) . '&view=agendaDay&date=' . $date->format('U')\r\n                    ) . $append\r\n                );\r\n            }\r\n\r\n            \/\/ limit listing\r\n            if ($idx >= 3) {\r\n                break;\r\n            }\r\n        }\r\n\r\n        \/\/ prepend event boxes to message body\r\n        if ($html) {\r\n            $this->ui->init();\r\n            $p['content'] = $html . $p['content'];\r\n            $this->rc->output->add_label('calendar.savingdata', 'calendar.deleteventconfirm', 'calendar.declinedeleteconfirm');\r\n        }\r\n\r\n        \/\/ add \"Save to calendar\" button into attachment menu\r\n        if ($has_events) {\r\n            $this->add_button(\r\n                [\r\n                    'id'         => 'attachmentsavecal',\r\n                    'name'       => 'attachmentsavecal',\r\n                    'type'       => 'link',\r\n                    'wrapper'    => 'li',\r\n                    'command'    => 'attachment-save-calendar',\r\n                    'class'      => 'icon calendarlink disabled',\r\n                    'classact'   => 'icon calendarlink active',\r\n                    'innerclass' => 'icon calendar',\r\n                    'label'      => 'calendar.savetocalendar',\r\n                ],\r\n                'attachmentmenu'\r\n            );\r\n        }\r\n\r\n        return $p;\r\n    }\r\n\r\n    \/**\r\n     * Handler for POST request to import an event attached to a mail message\r\n     *\/\r\n    public function mail_import_itip()\r\n    {\r\n        $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);\r\n\r\n        $uid     = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);\r\n        $mbox    = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);\r\n        $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);\r\n        $status  = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);\r\n        $delete  = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));\r\n        $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST));\r\n        $noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;\r\n        $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);\r\n        $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);\r\n        $comment  = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);\r\n\r\n        $error_msg = $this->gettext('errorimportingevent');\r\n        $success   = false;\r\n        $deleted   = false;\r\n        $dontsave  = false;\r\n        $existing = null;\r\n        $event_attendee = null;\r\n\r\n        if ($status == 'delegated') {\r\n            $to = rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true);\r\n            $delegates = rcube_mime::decode_address_list($to, 1, false);\r\n            $delegate  = reset($delegates);\r\n\r\n            if (empty($delegate) || empty($delegate['mailto'])) {\r\n                $this->rc->output->command('display_message', $this->rc->gettext('libcalendaring.delegateinvalidaddress'), 'error');\r\n                return;\r\n            }\r\n        }\r\n\r\n        \/\/ successfully parsed events?\r\n        if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {\r\n            \/\/ forward iTip request to delegatee\r\n            if (!empty($delegate)) {\r\n                $rsvpme = rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST);\r\n                $itip   = $this->load_itip();\r\n\r\n                $event['comment'] = $comment;\r\n\r\n                if ($itip->delegate_to($event, $delegate, !empty($rsvpme))) {\r\n                    $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');\r\n                } else {\r\n                    $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');\r\n                }\r\n\r\n                unset($event['comment']);\r\n\r\n                \/\/ the delegator is set to non-participant, thus save as non-blocking\r\n                $event['free_busy'] = 'free';\r\n            }\r\n\r\n            $mode = calendar_driver::FILTER_PERSONAL\r\n                | calendar_driver::FILTER_SHARED\r\n                | calendar_driver::FILTER_WRITEABLE;\r\n\r\n            \/\/ find writeable calendar to store event\r\n            $cal_id    = rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST);\r\n            $dontsave  = $cal_id === '' && $event['_method'] == 'REQUEST';\r\n            $calendars = $this->driver->list_calendars($mode);\r\n            $calendar  = $calendars[$cal_id] ?? null;\r\n\r\n            \/\/ select default calendar except user explicitly selected 'none'\r\n            if (!$calendar && !$dontsave) {\r\n                $calendar = $this->get_default_calendar($calendars);\r\n            }\r\n\r\n            $metadata = [\r\n                'uid'       => $event['uid'],\r\n                '_instance' => $event['_instance'] ?? null,\r\n                'changed'   => is_object($event['changed']) ? $event['changed']->format('U') : 0,\r\n                'sequence'  => intval($event['sequence'] ?? 0),\r\n                'fallback'  => strtoupper((string) $status),\r\n                'method'    => $event['_method'],\r\n                'task'      => 'calendar',\r\n            ];\r\n\r\n            \/\/ update my attendee status according to submitted method\r\n            if (!empty($status)) {\r\n                $organizer = null;\r\n                $emails = $this->get_user_emails();\r\n                foreach ($event['attendees'] as $i => $attendee) {\r\n                    $attendee_role = $attendee['role'] ?? null;\r\n                    $attendee_email = $attendee['email'] ?? null;\r\n\r\n                    if ($attendee_role == 'ORGANIZER') {\r\n                        $organizer = $attendee;\r\n                    } elseif ($attendee_email && in_array(strtolower($attendee_email), $emails)) {\r\n                        $event['attendees'][$i]['status'] = strtoupper($status);\r\n                        if (!in_array($event['attendees'][$i]['status'], ['NEEDS-ACTION', 'DELEGATED'])) {\r\n                            $event['attendees'][$i]['rsvp'] = false;  \/\/ unset RSVP attribute\r\n                        }\r\n\r\n                        $metadata['attendee'] = $attendee_email;\r\n                        $metadata['rsvp']     = $attendee_role != 'NON-PARTICIPANT';\r\n\r\n                        $reply_sender   = $attendee_email;\r\n                        $event_attendee = $attendee;\r\n                    }\r\n                }\r\n\r\n                \/\/ add attendee with this user's default identity if not listed\r\n                if (empty($reply_sender)) {\r\n                    $sender_identity = $this->rc->user->list_emails(true);\r\n                    $event['attendees'][] = [\r\n                        'name'   => $sender_identity['name'],\r\n                        'email'  => $sender_identity['email'],\r\n                        'role'   => 'OPT-PARTICIPANT',\r\n                        'status' => strtoupper($status),\r\n                    ];\r\n                    $metadata['attendee'] = $sender_identity['email'];\r\n                }\r\n            }\r\n\r\n            \/\/ save to calendar\r\n            if ($calendar && !empty($calendar['editable'])) {\r\n                \/\/ check for existing event with the same UID\r\n                $existing = $this->find_event($event, $mode);\r\n\r\n                \/\/ we'll create a new copy if user decided to change the calendar\r\n                if ($existing && $cal_id && $calendar['id'] != $existing['calendar']) {\r\n                    $existing = null;\r\n                }\r\n\r\n                \/\/ Use only free_busy values that make sense in this context (T853612)\r\n                if (!in_array($event['free_busy'] ?? '', ['free', 'busy'])) {\r\n                    unset($event['free_busy']);\r\n                }\r\n\r\n                $update_attendees = [];\r\n\r\n                if ($existing) {\r\n                    $calendar = $calendars[$existing['calendar']];\r\n\r\n                    \/\/ forward savemode for correct updates of recurring events\r\n                    $existing['_savemode'] = $savemode ?: (!empty($event['_savemode']) ? $event['_savemode'] : null);\r\n\r\n                    \/\/ only update attendee status\r\n                    if ($event['_method'] == 'REPLY') {\r\n                        $existing_attendee_index  = -1;\r\n\r\n                        if ($attendee = $this->itip->find_reply_attendee($event)) {\r\n                            $event_attendee       = $attendee;\r\n                            $update_attendees[]   = $attendee;\r\n                            $metadata['fallback'] = $attendee['status'];\r\n                            $metadata['attendee'] = $attendee['email'];\r\n                            $metadata['rsvp']     = !empty($attendee['rsvp']) || empty($attendee['role']) || $attendee['role'] != 'NON-PARTICIPANT';\r\n\r\n                            $existing_attendee_emails = [];\r\n\r\n                            \/\/ Find the attendee to update\r\n                            foreach ($existing['attendees'] as $i => $existing_attendee) {\r\n                                $existing_attendee_emails[] = $existing_attendee['email'];\r\n                                if ($this->itip->compare_email($existing_attendee['email'], $attendee['email'])) {\r\n                                    $existing_attendee_index = $i;\r\n                                }\r\n                            }\r\n\r\n                            if ($attendee['status'] == 'DELEGATED') {\r\n                                \/\/Also find and copy the delegatee\r\n                                $delegatee_email = $attendee['email'];\r\n                                $delegatees = array_filter($event['attendees'], function ($attendee) use ($delegatee_email) { return $attendee['role'] != 'ORGANIZER' && $this->itip->compare_email($attendee['delegated-from'], $delegatee_email); });\r\n\r\n                                if ($delegatee = $this->itip->find_attendee_by_email($event['attendees'], 'delegated-from', $attendee['email'])) {\r\n                                    $update_attendees[] = $delegatee;\r\n                                    if (!in_array_nocase($delegatee['email'], $existing_attendee_emails)) {\r\n                                        $existing['attendees'][] = $delegatee['email'];\r\n                                    }\r\n                                }\r\n                            }\r\n                        }\r\n\r\n                        \/\/ if delegatee has declined, set delegator's RSVP=True\r\n                        if ($event_attendee\r\n                            && $event_attendee['status'] == 'DECLINED'\r\n                            && !empty($event_attendee['delegated-from'])\r\n                        ) {\r\n                            foreach ($existing['attendees'] as $i => $attendee) {\r\n                                if ($attendee['email'] == $event_attendee['delegated-from']) {\r\n                                    $existing['attendees'][$i]['rsvp'] = true;\r\n                                    break;\r\n                                }\r\n                            }\r\n                        }\r\n\r\n                        \/\/ found matching attendee entry in both existing and new events\r\n                        if ($existing_attendee_index >= 0 && $event_attendee) {\r\n                            $existing['attendees'][$existing_attendee_index] = $event_attendee;\r\n                            $success = $this->driver->update_attendees($existing, $update_attendees);\r\n                        }\r\n                        \/\/ update the entire attendees block\r\n                        elseif (\r\n                            ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed'])\r\n                            && $event_attendee\r\n                        ) {\r\n                            $existing['attendees'][] = $event_attendee;\r\n                            $success = $this->driver->update_attendees($existing, $update_attendees);\r\n                        } elseif (!$event_attendee) {\r\n                            $error_msg = $this->gettext('errorunknownattendee');\r\n                        } else {\r\n                            $error_msg = $this->gettext('newerversionexists');\r\n                        }\r\n                    }\r\n                    \/\/ delete the event when declined (#1670)\r\n                    elseif ($status == 'declined' && $delete) {\r\n                        $deleted = $this->driver->remove_event($existing, true);\r\n                        $success = true;\r\n                    }\r\n                    \/\/ import the (newer) event\r\n                    elseif ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) {\r\n                        $event['id']       = $existing['id'];\r\n                        $event['calendar'] = $existing['calendar'];\r\n\r\n                        \/\/ merge attendees status\r\n                        \/\/ e.g. preserve my participant status for regular updates\r\n                        $this->lib->merge_attendees($event, $existing, $status);\r\n\r\n                        \/\/ set status=CANCELLED on CANCEL messages\r\n                        if ($event['_method'] == 'CANCEL') {\r\n                            $event['status'] = 'CANCELLED';\r\n                        }\r\n\r\n                        \/\/ update attachments list, allow attachments update only on REQUEST (#5342)\r\n                        if ($event['_method'] == 'REQUEST') {\r\n                            $event['deleted_attachments'] = true;\r\n                        } else {\r\n                            unset($event['attachments']);\r\n                        }\r\n\r\n                        \/\/ show me as free when declined (#1670)\r\n                        if ($status == 'declined'\r\n                            || (!empty($event['status']) && $event['status'] == 'CANCELLED')\r\n                            || ($event_attendee && ($event_attendee['role'] ?? '') == 'NON-PARTICIPANT')\r\n                        ) {\r\n                            $event['free_busy'] = 'free';\r\n                        }\r\n\r\n                        $success = $this->driver->edit_event($event);\r\n                    } elseif (!empty($status)) {\r\n                        $existing['attendees'] = $event['attendees'];\r\n                        if ($status == 'declined' || ($event_attendee && ($event_attendee['role'] ?? '') == 'NON-PARTICIPANT')) {\r\n                            \/\/ show me as free when declined (#1670)\r\n                            $existing['free_busy'] = 'free';\r\n                        }\r\n                        $success = $this->driver->edit_event($existing);\r\n                    } else {\r\n                        $error_msg = $this->gettext('newerversionexists');\r\n                    }\r\n                } elseif (empty($existing) && ($status != 'declined' || $this->rc->config->get('kolab_invitation_calendars'))) {\r\n                    if ($status == 'declined'\r\n                        || ($event['status'] ?? '') == 'CANCELLED'\r\n                        || ($event_attendee && ($event_attendee['role'] ?? '') == 'NON-PARTICIPANT')\r\n                    ) {\r\n                        $event['free_busy'] = 'free';\r\n                    }\r\n\r\n                    \/\/ if the RSVP reply only refers to a single instance:\r\n                    \/\/ store unmodified master event with current instance as exception\r\n                    if (!empty($instance) && !empty($savemode) && $savemode != 'all') {\r\n                        $master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event');\r\n                        if ($master['recurrence'] && empty($master['_instance'])) {\r\n                            \/\/ compute recurring events until this instance's date\r\n                            if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) {\r\n                                $recurrence_date->setTime(23, 59, 59);\r\n\r\n                                foreach ($this->driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) {\r\n                                    if ($recurring['_instance'] == $instance) {\r\n                                        \/\/ copy attendees block with my partstat to exception\r\n                                        $recurring['attendees'] = $event['attendees'];\r\n                                        $master['recurrence']['EXCEPTIONS'][] = $recurring;\r\n                                        $event = $recurring;  \/\/ set reference for iTip reply\r\n                                        break;\r\n                                    }\r\n                                }\r\n\r\n                                $master['calendar'] = $event['calendar'] = $calendar['id'];\r\n                                $success = $this->driver->new_event($master);\r\n                            } else {\r\n                                $master = null;\r\n                            }\r\n                        } else {\r\n                            $master = null;\r\n                        }\r\n                    }\r\n\r\n                    \/\/ save to the selected\/default calendar\r\n                    if (empty($master)) {\r\n                        $event['calendar'] = $calendar['id'];\r\n                        $success = $this->driver->new_event($event);\r\n                    }\r\n                } elseif ($status == 'declined') {\r\n                    $error_msg = null;\r\n                }\r\n            } elseif ($status == 'declined' || $dontsave) {\r\n                $error_msg = null;\r\n            } else {\r\n                $error_msg = $this->gettext('nowritecalendarfound');\r\n            }\r\n        }\r\n\r\n        if ($success) {\r\n            if ($event['_method'] == 'REPLY') {\r\n                $message = 'attendeupdateesuccess';\r\n            } else {\r\n                $message = $deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully');\r\n            }\r\n\r\n            $msg = $this->gettext(['name' => $message, 'vars' => ['calendar' => $calendar['name'] ?? '']]);\r\n            $this->rc->output->command('display_message', $msg, 'confirmation');\r\n        }\r\n\r\n        if ($success || $dontsave) {\r\n            $metadata['calendar'] = $event['calendar'] ?? null;\r\n            $metadata['nosave']   = $dontsave;\r\n            $metadata['rsvp']     = !empty($metadata['rsvp']);\r\n\r\n            $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);\r\n            $this->rc->output->command('plugin.itip_message_processed', $metadata);\r\n            $error_msg = null;\r\n        } elseif ($error_msg) {\r\n            $this->rc->output->command('display_message', $error_msg, 'error');\r\n        }\r\n\r\n        \/\/ send iTip reply\r\n        if (!empty($event) && $event['_method'] == 'REQUEST' && !empty($organizer) && !$noreply && !$error_msg && !empty($reply_sender)\r\n            && !in_array(strtolower($organizer['email']), $emails ?? [])\r\n        ) {\r\n            $event['comment'] = $comment;\r\n            $itip = $this->load_itip();\r\n            $itip->set_sender_email($reply_sender);\r\n\r\n            if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) {\r\n                $mailto = !empty($organizer['name']) ? $organizer['name'] : $organizer['email'];\r\n                $msg    = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);\r\n                $this->rc->output->command('display_message', $msg, 'confirmation');\r\n            } else {\r\n                $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');\r\n            }\r\n        }\r\n\r\n        $this->rc->output->send();\r\n    }\r\n\r\n    \/**\r\n     * Handler for calendar\/itip-remove requests\r\n     *\/\r\n    public function mail_itip_decline_reply()\r\n    {\r\n        $uid     = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);\r\n        $mbox    = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);\r\n        $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);\r\n\r\n        if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event'))\r\n            && $event['_method'] == 'REPLY'\r\n        ) {\r\n            $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);\r\n\r\n            foreach ($event['attendees'] as $_attendee) {\r\n                if ($_attendee['role'] != 'ORGANIZER') {\r\n                    $attendee = $_attendee;\r\n                    break;\r\n                }\r\n            }\r\n\r\n            $itip = $this->load_itip();\r\n\r\n            if ($itip->send_itip_message($event, 'CANCEL', $attendee ?? null, 'itipsubjectcancel', 'itipmailbodycancel')) {\r\n                $mailto = !empty($attendee['name']) ? $attendee['name'] : ($attendee['email'] ?? '');\r\n                $msg    = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);\r\n                $this->rc->output->command('display_message', $msg, 'confirmation');\r\n            } else {\r\n                $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');\r\n            }\r\n        } else {\r\n            $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Handler for calendar\/itip-delegate requests\r\n     *\/\r\n    public function mail_itip_delegate()\r\n    {\r\n        \/\/ forward request to mail_import_itip() with the right status\r\n        $_POST['_status'] = $_REQUEST['_status'] = 'delegated';\r\n        $this->mail_import_itip();\r\n    }\r\n\r\n    \/**\r\n     * Import the full payload from a mail message attachment\r\n     *\/\r\n    public function mail_import_attachment()\r\n    {\r\n        $uid     = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);\r\n        $mbox    = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);\r\n        $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);\r\n\r\n        \/\/ establish imap connection\r\n        $imap = $this->rc->get_storage();\r\n        $imap->set_folder($mbox);\r\n\r\n        if ($uid && $mime_id) {\r\n            $part = $imap->get_message_part($uid, $mime_id);\r\n\r\n            if ($part) {\r\n                $events = $this->get_ical()->import($part);\r\n            }\r\n        }\r\n\r\n        $success = $existing = 0;\r\n\r\n        if (!empty($events)) {\r\n            \/\/ find writeable calendar to store event\r\n            $cal_id = !empty($_REQUEST['_calendar']) ? rcube_utils::get_input_value('_calendar', rcube_utils::INPUT_POST) : null;\r\n            $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL);\r\n\r\n            foreach ($events as $event) {\r\n                \/\/ save to calendar\r\n                $calendar = !empty($calendars[$cal_id]) ? $calendars[$cal_id] : $this->get_default_calendar();\r\n                if ($calendar && $calendar['editable'] && $event['_type'] == 'event') {\r\n                    $event['calendar'] = $calendar['id'];\r\n\r\n                    if (!$this->driver->get_event($event['uid'], calendar_driver::FILTER_WRITEABLE)) {\r\n                        $success += (bool)$this->driver->new_event($event);\r\n                    } else {\r\n                        $existing++;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        if ($success) {\r\n            $msg = $this->gettext(['name' => 'importsuccess', 'vars' => ['nr' => $success]]);\r\n            $this->rc->output->command('display_message', $msg, 'confirmation');\r\n        } elseif ($existing) {\r\n            $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');\r\n        } else {\r\n            $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Read email message and return contents for a new event based on that message\r\n     *\/\r\n    public function mail_message2event()\r\n    {\r\n        $this->ui->init();\r\n        $this->ui->addJS();\r\n        $this->ui->init_templates();\r\n        $this->ui->calendar_list([], true); \/\/ set env['calendars']\r\n\r\n        $uid   = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);\r\n        $mbox  = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GET);\r\n        $event = [];\r\n\r\n        \/\/ establish imap connection\r\n        $imap    = $this->rc->get_storage();\r\n        $message = new rcube_message($uid, $mbox);\r\n\r\n        if ($message->headers) {\r\n            $event['title']       = trim($message->subject);\r\n            $event['description'] = trim($message->first_text_part());\r\n\r\n            $this->load_driver();\r\n\r\n            \/\/ add a reference to the email message\r\n            if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {\r\n                $event['links'] = [$msgref];\r\n            }\r\n            \/\/ copy mail attachments to event\r\n            elseif (!empty($message->attachments) && !empty($this->driver->attachments)) {\r\n                $handler = new kolab_attachments_handler();\r\n                $event['attachments'] = $handler->copy_mail_attachments(self::SESSION_KEY, 'cal-', $message);\r\n            }\r\n\r\n            $this->rc->output->set_env('event_prop', $event);\r\n        } else {\r\n            $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');\r\n        }\r\n\r\n        $this->rc->output->send('calendar.dialog');\r\n    }\r\n\r\n    \/**\r\n     * Handler for the 'message_compose' plugin hook. This will check for\r\n     * a compose parameter 'calendar_event' and create an attachment with the\r\n     * referenced event in iCal format\r\n     *\/\r\n    public function mail_message_compose($args)\r\n    {\r\n        \/\/ set the submitted event ID as attachment\r\n        if (!empty($args['param']['calendar_event'])) {\r\n            $this->load_driver();\r\n\r\n            [$cal, $id] = explode(':', $args['param']['calendar_event'], 2);\r\n\r\n            if ($event = $this->driver->get_event(['id' => $id, 'calendar' => $cal])) {\r\n                $filename = asciiwords($event['title']);\r\n                if (empty($filename)) {\r\n                    $filename = 'event';\r\n                }\r\n\r\n                \/\/ save ics to a temp file and register as attachment\r\n                $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal');\r\n                $export   = $this->get_ical()->export([$event], '', false, [$this->driver, 'get_attachment_body']);\r\n\r\n                file_put_contents($tmp_path, $export);\r\n\r\n                $args['attachments'][] = [\r\n                    'path'     => $tmp_path,\r\n                    'name'     => $filename . '.ics',\r\n                    'mimetype' => 'text\/calendar',\r\n                    'size'     => filesize($tmp_path),\r\n                ];\r\n                $args['param']['subject'] = $event['title'];\r\n            }\r\n        }\r\n\r\n        return $args;\r\n    }\r\n\r\n    \/**\r\n     * Create a Nextcould Talk room\r\n     *\/\r\n    public function talk_room_create()\r\n    {\r\n        require_once __DIR__ . '\/lib\/calendar_nextcloud_api.php';\r\n\r\n        $api = new calendar_nextcloud_api();\r\n\r\n        $name = (string) rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST);\r\n\r\n        $room_url = $api->talk_room_create($name);\r\n\r\n        if ($room_url) {\r\n            $this->rc->output->command('plugin.talk_room_created', ['url' => $room_url]);\r\n        } else {\r\n            $this->rc->output->command('display_message', $this->gettext('talkroomcreateerror'), 'error');\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Update a Nextcould Talk room\r\n     *\/\r\n    public function talk_room_update($event)\r\n    {\r\n        \/\/ If a room is assigned to the event...\r\n        if (\r\n            ($talk_url = $this->rc->config->get('calendar_nextcloud_url'))\r\n            && isset($event['attendees'])\r\n            && !empty($event['location'])\r\n            && strpos($event['location'], unslashify($talk_url) . '\/call\/') === 0\r\n        ) {\r\n            $participants = [];\r\n            $organizer = null;\r\n\r\n            \/\/ ollect participants' and organizer's email addresses\r\n            foreach ($event['attendees'] as $attendee) {\r\n                if (!empty($attendee['email'])) {\r\n                    if ($attendee['role'] == 'ORGANIZER') {\r\n                        $organizer = $attendee['email'];\r\n                    } elseif ($attendee['cutype'] == 'INDIVIDUAL') {\r\n                        $participants[] = $attendee['email'];\r\n                    }\r\n                }\r\n            }\r\n\r\n            \/\/ If the event is owned by the current user update the room\r\n            if ($organizer && in_array($organizer, $this->get_user_emails())) {\r\n                require_once __DIR__ . '\/lib\/calendar_nextcloud_api.php';\r\n\r\n                $api = new calendar_nextcloud_api();\r\n\r\n                $api->talk_room_update($event['location'], $participants);\r\n            }\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Get a list of email addresses of the current user (from login and identities)\r\n     *\/\r\n    public function get_user_emails()\r\n    {\r\n        return $this->lib->get_user_emails();\r\n    }\r\n\r\n    \/**\r\n     * Build an absolute URL with the given parameters\r\n     *\/\r\n    public function get_url($param = [])\r\n    {\r\n        $param += ['task' => 'calendar'];\r\n        return $this->rc->url($param, true, true);\r\n    }\r\n\r\n    public function ical_feed_hash($source)\r\n    {\r\n        return base64_encode($this->rc->user->get_username() . ':' . $source);\r\n    }\r\n\r\n    \/**\r\n     * Handler for user_delete plugin hook\r\n     *\/\r\n    public function user_delete($args)\r\n    {\r\n        \/\/ delete itipinvitations entries related to this user\r\n        $db = $this->rc->get_dbh();\r\n        $table_itipinvitations = $db->table_name('itipinvitations', true);\r\n\r\n        $db->query(\"DELETE FROM $table_itipinvitations WHERE `user_id` = ?\", $args['user']->ID);\r\n\r\n        $this->setup();\r\n        $this->load_driver();\r\n\r\n        return $this->driver->user_delete($args);\r\n    }\r\n\r\n    \/**\r\n     * Find first occurrence of a recurring event excluding start date\r\n     *\r\n     * @param array $event Event data (with 'start' and 'recurrence')\r\n     *\r\n     * @return DateTime Date of the first occurrence\r\n     *\/\r\n    public function find_first_occurrence($event)\r\n    {\r\n        \/\/ Make sure libkolab\/libcalendaring plugins are loaded\r\n        $this->load_driver();\r\n\r\n        $driver_name = $this->rc->config->get('calendar_driver', 'database');\r\n\r\n        \/\/ Use kolabcalendaring\/kolabformat to compute recurring events only with the Kolab driver\r\n        if ($driver_name == 'kolab' && class_exists('kolabformat') && class_exists('kolabcalendaring')\r\n            && class_exists('kolab_date_recurrence')\r\n        ) {\r\n            $object = kolab_format::factory('event', 3.0);\r\n            $object->set($event);\r\n\r\n            $recurrence = new kolab_date_recurrence($object);\r\n        } else {\r\n            \/\/ fallback to libcalendaring recurrence implementation\r\n            $recurrence = new libcalendaring_recurrence($this->lib, $event);\r\n        }\r\n\r\n        return $recurrence->first_occurrence();\r\n    }\r\n\r\n    \/**\r\n     * Get date-time input from UI and convert to unix timestamp\r\n     *\/\r\n    protected function input_timestamp($name, $type)\r\n    {\r\n        $ts = rcube_utils::get_input_value($name, $type);\r\n\r\n        if ($ts && (!is_numeric($ts) || strpos($ts, 'T'))) {\r\n            $ts = new DateTime($ts, $this->timezone);\r\n            $ts = $ts->getTimestamp();\r\n        }\r\n\r\n        return $ts;\r\n    }\r\n\r\n    \/**\r\n     * Magic getter for public access to protected members\r\n     *\/\r\n    public function __get($name)\r\n    {\r\n        switch ($name) {\r\n            case 'ical':\r\n                return $this->get_ical();\r\n\r\n            case 'itip':\r\n                return $this->load_itip();\r\n\r\n            case 'driver':\r\n                $this->load_driver();\r\n                return $this->driver;\r\n        }\r\n\r\n        return null;\r\n    }\r\n}\n\t\t<\/div>\n\t<\/div>\n<\/div><\/div><\/div><\/div>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"page-templates\/blank-page.php","meta":{"footnotes":""},"class_list":["post-1035","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/gotravindo.com\/id\/wp-json\/wp\/v2\/pages\/1035","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/gotravindo.com\/id\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/gotravindo.com\/id\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/gotravindo.com\/id\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/gotravindo.com\/id\/wp-json\/wp\/v2\/comments?post=1035"}],"version-history":[{"count":17,"href":"https:\/\/gotravindo.com\/id\/wp-json\/wp\/v2\/pages\/1035\/revisions"}],"predecessor-version":[{"id":1052,"href":"https:\/\/gotravindo.com\/id\/wp-json\/wp\/v2\/pages\/1035\/revisions\/1052"}],"wp:attachment":[{"href":"https:\/\/gotravindo.com\/id\/wp-json\/wp\/v2\/media?parent=1035"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}