diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index cbf8b0ee..43818247 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -2027,6 +2027,10 @@ class calendar extends rcube_plugin */ private function write_preprocess(&$event, $action) { + // Remove double timezone specification (T2313) + $event['start'] = preg_replace('/\s*\(.*\)/', '', $event['start']); + $event['end'] = preg_replace('/\s*\(.*\)/', '', $event['end']); + // convert dates into DateTime objects in user's current timezone $event['start'] = new DateTime($event['start'], $this->timezone); $event['end'] = new DateTime($event['end'], $this->timezone); diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index fd431f3e..6ee33117 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -3104,7 +3104,7 @@ function rcube_calendar_ui(settings) else if (range > 0) start = 'today -' + range + ' months'; - rcmail.goto_url('export_events', { source:source, start:start, attachments:attachmt?1:0 }); + rcmail.goto_url('export_events', { source:source, start:start, attachments:attachmt?1:0 }, false); } $dialog.dialog("close"); } @@ -3135,7 +3135,7 @@ function rcube_calendar_ui(settings) this.event_download = function(event) { if (event && event.id) { - rcmail.goto_url('export_events', { source:event.calendar, id:event.id, attachments:1 }); + rcmail.goto_url('export_events', { source:event.calendar, id:event.id, attachments:1 }, false); } }; diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php index 8e1d17f3..e2371187 100644 --- a/plugins/libcalendaring/lib/libcalendaring_itip.php +++ b/plugins/libcalendaring/lib/libcalendaring_itip.php @@ -192,7 +192,7 @@ class libcalendaring_itip } } - return $p; + return $p; } /** @@ -365,128 +365,153 @@ class libcalendaring_itip */ public function get_itip_status($event, $existing = null) { - $action = $event['rsvp'] ? 'rsvp' : ''; - $status = $event['fallback']; - $latest = $rescheduled = false; - $html = ''; + $action = $event['rsvp'] ? 'rsvp' : ''; + $status = $event['fallback']; + $latest = $rescheduled = false; + $html = ''; - if (is_numeric($event['changed'])) - $event['changed'] = new DateTime('@'.$event['changed']); + if (is_numeric($event['changed'])) { + $event['changed'] = new DateTime('@'.$event['changed']); + } - // check if the given itip object matches the last state - if ($existing) { - $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) || + // check if the given itip object matches the last state + if ($existing) { + $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']); - } + } - // determine action for REQUEST - if ($event['method'] == 'REQUEST') { - $html = html::div('rsvp-status', $this->gettext('acceptinvitation')); + // determine action for REQUEST + if ($event['method'] == 'REQUEST') { + $html = html::div('rsvp-status', $this->gettext('acceptinvitation')); - if ($existing) { - $rsvp = $event['rsvp']; - $emails = $this->lib->get_user_emails(); - foreach ($existing['attendees'] as $attendee) { - if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { - $status = strtoupper($attendee['status']); - break; + if ($existing) { + $rsvp = $event['rsvp']; + $emails = $this->lib->get_user_emails(); + + foreach ($existing['attendees'] as $attendee) { + if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { + $status = strtoupper($attendee['status']); + break; + } + } + + // Detect re-sheduling + if (!$latest) { + // FIXME: This is probably to simplistic, or maybe we should just check + // attendee's RSVP flag in the new event? + $rescheduled = $existing['start'] != $event['start'] || $existing['end'] > $event['end']; + } } - } - - // Detect re-sheduling - if (!$latest) { - // FIXME: This is probably to simplistic, or maybe we should just check - // attendee's RSVP flag in the new event? - $rescheduled = $existing['start'] != $event['start'] || $existing['end'] > $event['end']; - } - } - else { - $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true); - } - - $status_lc = strtolower($status); - - if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) { - $html = html::div('rsvp-status', $this->gettext('notanattendee')); - $action = 'import'; - } - else if (in_array($status_lc, $this->rsvp_status)) { - $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc); - - if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) { - $action = ''; // nothing to do here, outdated invitation - if ($status_lc == 'needs-action') - $status_text = $this->gettext('outdatedinvitation'); - } - else if (!$existing && !$rsvp) { - $action = 'import'; - } - else if ($rescheduled) { - $action = 'rsvp'; - } - else if ($status_lc != 'needs-action') { - // check if there are any changes - if ($latest) { - $diff = $this->get_itip_diff($event, $existing); - $latest = empty($diff); + else { + $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true); } - $action = !$latest ? 'update' : ''; - } + $status_lc = strtolower($status); - $html = html::div('rsvp-status ' . $status_lc, $status_text); - } - } - // determine action for REPLY - else if ($event['method'] == 'REPLY') { - // check whether the sender already is an attendee - if ($existing) { - $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : ''; - $listed = false; - foreach ($existing['attendees'] as $attendee) { - if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) { - $status_lc = strtolower($status); - if (in_array($status_lc, $this->rsvp_status)) { - $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array( - 'name' => 'attendee' . $status_lc, - 'vars' => array( - 'delegatedto' => rcube::Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')), - ) - ))); - } - $action = $attendee['status'] == $status || !$latest ? '' : 'update'; - $listed = true; - break; + if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) { + $html = html::div('rsvp-status', $this->gettext('notanattendee')); + $action = 'import'; } - } + else if (in_array($status_lc, $this->rsvp_status)) { + $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc); - if (!$listed) { - $html = html::div('rsvp-status', $this->gettext('itipnewattendee')); - } - } - else { - $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); - $action = ''; - } - } - else if ($event['method'] == 'CANCEL') { - if (!$existing) { - $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); - $action = ''; - } - } + if ($existing && ($existing['sequence'] > $event['sequence'] + || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed'])) + ) { + $action = ''; // nothing to do here, outdated invitation + if ($status_lc == 'needs-action') { + $status_text = $this->gettext('outdatedinvitation'); + } + } + else if (!$existing && !$rsvp) { + $action = 'import'; + } + else if ($rescheduled) { + $action = 'rsvp'; + } + else if ($status_lc != 'needs-action') { + // check if there are any changes + if ($latest) { + $diff = $this->get_itip_diff($event, $existing); + $latest = empty($diff); + } - return array( - 'uid' => $event['uid'], - 'id' => asciiwords($event['uid'], true), - 'existing' => $existing ? true : false, - 'saved' => $existing ? true : false, - 'latest' => $latest, - 'status' => $status, - 'action' => $action, - 'rescheduled' => $rescheduled, - 'html' => $html, - ); + $action = !$latest ? 'update' : ''; + } + + $html = html::div('rsvp-status ' . $status_lc, $status_text); + } + } + // determine action for REPLY + else if ($event['method'] == 'REPLY') { + // check whether the sender already is an attendee + if ($existing) { + // Relax checking if that is a reply to the latest version of the event + // We accept versions with older SEQUENCE but no significant changes (Bifrost#T78144) + if (!$latest) { + $num = $got = 0; + foreach (array('start', 'end', 'due', 'allday', 'recurrence', 'location') as $key) { + if (isset($existing[$key])) { + if ($key == 'allday') { + $event[$key] = $event[$key] == 'true'; + } + $value = $existing[$key] instanceof DateTime ? $existing[$key]->format('c') : $existing[$key]; + $num++; + $got += intval($value == $event[$key]); + } + } + + $latest = $num === $got; + } + + $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : ''; + $listed = false; + + foreach ($existing['attendees'] as $attendee) { + if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) { + $status_lc = strtolower($status); + if (in_array($status_lc, $this->rsvp_status)) { + $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array( + 'name' => 'attendee' . $status_lc, + 'vars' => array( + 'delegatedto' => rcube::Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')), + ) + ))); + } + + $action = $attendee['status'] == $status || !$latest ? '' : 'update'; + $listed = true; + break; + } + } + + if (!$listed) { + $html = html::div('rsvp-status', $this->gettext('itipnewattendee')); + } + } + else { + $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); + $action = ''; + } + } + else if ($event['method'] == 'CANCEL') { + if (!$existing) { + $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); + $action = ''; + } + } + + return array( + 'uid' => $event['uid'], + 'id' => asciiwords($event['uid'], true), + 'existing' => $existing ? true : false, + 'saved' => $existing ? true : false, + 'latest' => $latest, + 'status' => $status, + 'action' => $action, + 'rescheduled' => $rescheduled, + 'html' => $html, + ); } protected function get_itip_diff($event, $existing) @@ -612,6 +637,13 @@ class libcalendaring_itip $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button); $buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons); + + // For replies we need more metadata + foreach (array('start', 'end', 'due', 'allday', 'recurrence', 'location') as $key) { + if (isset($event[$key])) { + $metadata[$key] = $event[$key] instanceof DateTime ? $event[$key]->format('c') : $event[$key]; + } + } } // when receiving iTip REQUEST messages: else if ($method == 'REQUEST') { diff --git a/plugins/libkolab/SQL/mysql.initial.sql b/plugins/libkolab/SQL/mysql.initial.sql index 4f4f5262..0213c45b 100644 --- a/plugins/libkolab/SQL/mysql.initial.sql +++ b/plugins/libkolab/SQL/mysql.initial.sql @@ -29,7 +29,7 @@ DROP TABLE IF EXISTS `kolab_cache_contact`; CREATE TABLE `kolab_cache_contact` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, - `uid` VARCHAR(512) CHARACTER SET ascii NOT NULL, + `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, @@ -53,7 +53,7 @@ DROP TABLE IF EXISTS `kolab_cache_event`; CREATE TABLE `kolab_cache_event` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, - `uid` VARCHAR(512) CHARACTER SET ascii NOT NULL, + `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, @@ -73,7 +73,7 @@ DROP TABLE IF EXISTS `kolab_cache_task`; CREATE TABLE `kolab_cache_task` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, - `uid` VARCHAR(512) CHARACTER SET ascii NOT NULL, + `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, @@ -93,7 +93,7 @@ DROP TABLE IF EXISTS `kolab_cache_journal`; CREATE TABLE `kolab_cache_journal` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, - `uid` VARCHAR(512) CHARACTER SET ascii NOT NULL, + `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, @@ -113,7 +113,7 @@ DROP TABLE IF EXISTS `kolab_cache_note`; CREATE TABLE `kolab_cache_note` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, - `uid` VARCHAR(512) CHARACTER SET ascii NOT NULL, + `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, @@ -131,7 +131,7 @@ DROP TABLE IF EXISTS `kolab_cache_file`; CREATE TABLE `kolab_cache_file` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, - `uid` VARCHAR(512) CHARACTER SET ascii NOT NULL, + `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, @@ -151,7 +151,7 @@ DROP TABLE IF EXISTS `kolab_cache_configuration`; CREATE TABLE `kolab_cache_configuration` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, - `uid` VARCHAR(512) CHARACTER SET ascii NOT NULL, + `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, @@ -171,7 +171,7 @@ DROP TABLE IF EXISTS `kolab_cache_freebusy`; CREATE TABLE `kolab_cache_freebusy` ( `folder_id` BIGINT UNSIGNED NOT NULL, `msguid` BIGINT UNSIGNED NOT NULL, - `uid` VARCHAR(512) CHARACTER SET ascii NOT NULL, + `uid` VARCHAR(512) NOT NULL, `created` DATETIME DEFAULT NULL, `changed` DATETIME DEFAULT NULL, `data` LONGTEXT NOT NULL, @@ -188,4 +188,4 @@ CREATE TABLE `kolab_cache_freebusy` ( /*!40014 SET FOREIGN_KEY_CHECKS=1 */; -REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2017071900'); +REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2018021300'); diff --git a/plugins/libkolab/SQL/mysql/2018021300.sql b/plugins/libkolab/SQL/mysql/2018021300.sql new file mode 100644 index 00000000..9b5981d1 --- /dev/null +++ b/plugins/libkolab/SQL/mysql/2018021300.sql @@ -0,0 +1,9 @@ +-- accept utf8 in UID column +ALTER TABLE `kolab_cache_contact` MODIFY `uid` VARCHAR(512) CHARACTER SET utf8 NOT NULL; +ALTER TABLE `kolab_cache_event` MODIFY `uid` VARCHAR(512) CHARACTER SET utf8 NOT NULL; +ALTER TABLE `kolab_cache_task` MODIFY `uid` VARCHAR(512) CHARACTER SET utf8 NOT NULL; +ALTER TABLE `kolab_cache_journal` MODIFY `uid` VARCHAR(512) CHARACTER SET utf8 NOT NULL; +ALTER TABLE `kolab_cache_note` MODIFY `uid` VARCHAR(512) CHARACTER SET utf8 NOT NULL; +ALTER TABLE `kolab_cache_file` MODIFY `uid` VARCHAR(512) CHARACTER SET utf8 NOT NULL; +ALTER TABLE `kolab_cache_configuration` MODIFY `uid` VARCHAR(512) CHARACTER SET utf8 NOT NULL; +ALTER TABLE `kolab_cache_freebusy` MODIFY `uid` VARCHAR(512) CHARACTER SET utf8 NOT NULL; diff --git a/plugins/libkolab/SQL/oracle.initial.sql b/plugins/libkolab/SQL/oracle.initial.sql index 77a16ee3..78675783 100644 --- a/plugins/libkolab/SQL/oracle.initial.sql +++ b/plugins/libkolab/SQL/oracle.initial.sql @@ -183,4 +183,4 @@ CREATE TABLE "kolab_cache_freebusy" ( CREATE INDEX "kolab_cache_fb_uid2msguid" ON "kolab_cache_freebusy" ("folder_id", "uid", "msguid"); -INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2017071900'); +INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2018021300'); diff --git a/plugins/libkolab/SQL/oracle/2018021300.sql b/plugins/libkolab/SQL/oracle/2018021300.sql new file mode 100644 index 00000000..fe6741a0 --- /dev/null +++ b/plugins/libkolab/SQL/oracle/2018021300.sql @@ -0,0 +1 @@ +-- empty \ No newline at end of file diff --git a/plugins/libkolab/SQL/sqlite.initial.sql b/plugins/libkolab/SQL/sqlite.initial.sql index 7a56636f..909188ca 100644 --- a/plugins/libkolab/SQL/sqlite.initial.sql +++ b/plugins/libkolab/SQL/sqlite.initial.sql @@ -156,4 +156,4 @@ CREATE TABLE kolab_cache_freebusy ( CREATE INDEX ix_freebusy_uid2msguid ON kolab_cache_freebusy(folder_id,uid,msguid); -INSERT INTO system (name, value) VALUES ('libkolab-version', '2017071900'); +INSERT INTO system (name, value) VALUES ('libkolab-version', '2018021300'); diff --git a/plugins/libkolab/SQL/sqlite/2018021300.sql b/plugins/libkolab/SQL/sqlite/2018021300.sql new file mode 100644 index 00000000..fe6741a0 --- /dev/null +++ b/plugins/libkolab/SQL/sqlite/2018021300.sql @@ -0,0 +1 @@ +-- empty \ No newline at end of file