From d854a5ab0d4a460d21ca6a201fb2a9be25488aad Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 13 Mar 2013 16:19:02 +0100 Subject: [PATCH 1/6] Keep libkolab API for event recurrence exceptions consistent for Kolab v2. The format doesn't allow to save exceptions inline, thus save them as individual events --- plugins/libkolab/lib/kolab_format_event.php | 2 +- plugins/libkolab/lib/kolab_storage_folder.php | 59 ++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php index 2b3f0c8f..ec97767c 100644 --- a/plugins/libkolab/lib/kolab_format_event.php +++ b/plugins/libkolab/lib/kolab_format_event.php @@ -192,7 +192,7 @@ class kolab_format_event extends kolab_format_xcal } // read exception event objects - if (($exceptions = $this->obj->exceptions()) && $exceptions->size()) { + if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) { for ($i=0; $i < $exceptions->size(); $i++) { if (($exobj = $exceptions->get($i))) { $exception = new kolab_format_event($exobj); diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php index 7e10f2e6..e4fc5de0 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -548,9 +548,9 @@ class kolab_storage_folder // detect old Kolab 2.0 format if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false) - $format_version = 2.0; + $format_version = '2.0'; else - $format_version = 3.0; // assume 3.0 + $format_version = '3.0'; // assume 3.0 } // get Kolab format handler for the given type @@ -588,7 +588,6 @@ class kolab_storage_folder return false; } - /** * Save an object in this folder. * @@ -659,6 +658,11 @@ class kolab_storage_folder } } + // save recurrence exceptions as individual objects due to lack of support in Kolab v2 format + if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) { + $this->save_recurrence_exceptions($object, $type); + } + // check IMAP BINARY extension support for 'file' objects // allow configuration to workaround bug in Cyrus < 2.4.17 $rcmail = rcube::get_instance(); @@ -666,6 +670,12 @@ class kolab_storage_folder // generate and save object message if ($raw_msg = $this->build_message($object, $type, $binary)) { + // resolve old msguid before saving + if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) { + $object['_msguid'] = $msguid; + $object['_mailbox'] = $this->name; + } + if (is_array($raw_msg)) { $result = $this->imap->save_message($this->name, $raw_msg[0], $raw_msg[1], true, null, null, $binary); @unlink($raw_msg[0]); @@ -679,10 +689,6 @@ class kolab_storage_folder $this->imap->delete_message($object['_msguid'], $object['_mailbox']); $this->cache->set($object['_msguid'], false, $object['_mailbox']); } - else if ($result && $uid && ($msguid = $this->cache->uid2msguid($uid))) { - $this->imap->delete_message($msguid, $this->name); - $this->cache->set($object['_msguid'], false); - } // update cache with new UID if ($result) { @@ -694,6 +700,45 @@ class kolab_storage_folder return $result; } + /** + * Save recurrence exceptions as individual objects. + * The Kolab v2 format doesn't allow us to save fully embedded exception objects. + * + * @param array Hash array with event properties + * @param string Object type + */ + private function save_recurrence_exceptions(&$object, $type = null) + { + if ($object['recurrence']['EXCEPTIONS']) { + $exdates = array(); + foreach ((array)$object['recurrence']['EXDATE'] as $exdate) { + $key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate); + $exdates[$key] = 1; + } + + // save every exception as individual object + foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) { + $exception['uid'] = $object['uid'] . '-' . $exception['start']->format('Ymd'); + $exception['recurrence-id'] = $exception['start']->format('Y-m-d'); + $exception['sequence'] = $object['sequence'] + 1; + + unset($exception['recurrence'], $exception['organizer'], $exception['attendees']); + $this->save($exception, $type, $exception['uid']); + + // set UNTIL date if we have a thisandfuture exception + if ($exception['thisandfuture']) { + $untildate = clone $exception['start']; + $untildate->sub(new DateInterval('P1D')); + $object['recurrence']['UNTIL'] = $untildate; + unset($object['recurrence']['COUNT']); + } + else if (!$exdates[$exception['start']->format('Y-m-d')]) + $object['recurrence']['EXDATE'][] = clone $exception['start']; + } + + unset($object['recurrence']['EXCEPTIONS']); + } + } /** * Delete the specified object from this folder. From 7bf5f2710537fac6737b1dccab90bda37c23d143 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Wed, 13 Mar 2013 17:33:45 +0100 Subject: [PATCH 2/6] Fix files list widget overflow --- plugins/kolab_files/skins/larry/style.css | 9 +++++++++ plugins/kolab_files/skins/larry/templates/files.html | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/plugins/kolab_files/skins/larry/style.css b/plugins/kolab_files/skins/larry/style.css index d4c86285..6a72335f 100644 --- a/plugins/kolab_files/skins/larry/style.css +++ b/plugins/kolab_files/skins/larry/style.css @@ -63,6 +63,15 @@ overflow: auto; } +#filelistbox { + bottom: 28px; + overflow: auto; + top: 0; + left: 0; + position: absolute; + width: 100%; +} + #files-folder-list ul li span.name { background: url(../../../../skins/larry/images/listicons.png) 6px 3px no-repeat; padding: 6px 8px 2px 32px; diff --git a/plugins/kolab_files/skins/larry/templates/files.html b/plugins/kolab_files/skins/larry/templates/files.html index c91f64bb..1f23e654 100644 --- a/plugins/kolab_files/skins/larry/templates/files.html +++ b/plugins/kolab_files/skins/larry/templates/files.html @@ -40,8 +40,10 @@ -
- +
+
+ +
From 69bc310cc53645550704a05998f9528769962cd5 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 13 Mar 2013 17:39:41 +0100 Subject: [PATCH 3/6] Fix saving of recurrence exceptions for v2: create unique UIDs, correctly save this-and-future instances with (modified) recurrence rule --- plugins/libkolab/lib/kolab_storage_folder.php | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php index e4fc5de0..dd0e8d2d 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -718,28 +718,51 @@ class kolab_storage_folder // save every exception as individual object foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) { - $exception['uid'] = $object['uid'] . '-' . $exception['start']->format('Ymd'); - $exception['recurrence-id'] = $exception['start']->format('Y-m-d'); + $exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd')); $exception['sequence'] = $object['sequence'] + 1; - unset($exception['recurrence'], $exception['organizer'], $exception['attendees']); - $this->save($exception, $type, $exception['uid']); - - // set UNTIL date if we have a thisandfuture exception if ($exception['thisandfuture']) { + $exception['recurrence'] = $object['recurrence']; + + // adjust the recurrence duration of the exception + if ($object['recurrence']['COUNT']) { + $recurrence = new kolab_date_recurrence($object['_formatobj']); + if ($end = $recurrence->end()) { + unset($exception['recurrence']['COUNT']); + $exception['recurrence']['UNTIL'] = new DateTime('@'.$end); + } + } + + // set UNTIL date if we have a thisandfuture exception $untildate = clone $exception['start']; $untildate->sub(new DateInterval('P1D')); $object['recurrence']['UNTIL'] = $untildate; unset($object['recurrence']['COUNT']); } - else if (!$exdates[$exception['start']->format('Y-m-d')]) - $object['recurrence']['EXDATE'][] = clone $exception['start']; + else { + if (!$exdates[$exception['start']->format('Y-m-d')]) + $object['recurrence']['EXDATE'][] = clone $exception['start']; + unset($exception['recurrence']); + } + + unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']); + $this->save($exception, $type, $exception['uid']); } unset($object['recurrence']['EXCEPTIONS']); } } + /** + * Generate an object UID with the given recurrence-ID in a way that it is + * unique (the original UID is not a substring) but still recoverable. + */ + private static function recurrence_exception_uid($uid, $recurrence_id) + { + $offset = -2; + return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset); + } + /** * Delete the specified object from this folder. * From 6000fd925ff09bf160a36f9c1ed316857831d568 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Fri, 15 Mar 2013 13:34:33 +0100 Subject: [PATCH 4/6] Fix upload button in IE --- plugins/kolab_files/skins/larry/ui.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/kolab_files/skins/larry/ui.js b/plugins/kolab_files/skins/larry/ui.js index a0a3c494..760aaf34 100644 --- a/plugins/kolab_files/skins/larry/ui.js +++ b/plugins/kolab_files/skins/larry/ui.js @@ -105,10 +105,10 @@ function kolab_files_upload_input(button) // opacity:0 does the trick, display/visibility doesn't work .css({opacity: 0, cursor: 'pointer', position: 'absolute', top: '10000px', left: '10000px'}); - // In FF we need to move the browser file-input's button under the cursor + // In FF and IE we need to move the browser file-input's button under the cursor // Thanks to the size attribute above we know the length of the input field - if (bw.mz) - file.css({marginLeft: '-75px'}); + if (bw.mz || bw.ie) + file.css({marginLeft: '-80px'}); // Note: now, I observe problem with cursor style on FF < 4 only link.css({overflow: 'hidden', cursor: 'pointer'}) From 2358a0d80728e68784ab118bcbbde52dad11a34e Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Fri, 15 Mar 2013 15:12:45 +0100 Subject: [PATCH 5/6] Implemented files moving (drag'n'drop) --- plugins/kolab_files/kolab_files.js | 134 ++++++++++++------ .../kolab_files/lib/kolab_files_engine.php | 3 +- plugins/kolab_files/localization/en_US.inc | 2 + 3 files changed, 96 insertions(+), 43 deletions(-) diff --git a/plugins/kolab_files/kolab_files.js b/plugins/kolab_files/kolab_files.js index 1b2fc6cb..e7bd0662 100644 --- a/plugins/kolab_files/kolab_files.js +++ b/plugins/kolab_files/kolab_files.js @@ -69,8 +69,8 @@ window.rcmail && rcmail.addEventListener('init', function() { /* rcmail.file_list.addEventListener('dragstart', function(o){ p.drag_start(o); }); rcmail.file_list.addEventListener('dragmove', function(e){ p.drag_move(e); }); - rcmail.file_list.addEventListener('dragend', function(e){ p.drag_end(e); }); */ + rcmail.file_list.addEventListener('dragend', function(e){ kolab_files_drag_end(e); }); rcmail.file_list.addEventListener('column_replace', function(e){ kolab_files_set_coltypes(e); }); rcmail.file_list.addEventListener('listupdate', function(e){ rcmail.triggerEvent('listupdate', e); }); @@ -356,6 +356,17 @@ kolab_files_list_select = function(list) // rcmail.select_all_mode = false; }; +kolab_files_drag_end = function() +{ + var folder = $('#files-folder-list li.droptarget').removeClass('droptarget'); + + if (folder.length) { + folder = folder.data('folder'); + + file_api.file_move(kolab_files_selected(), folder); + } +}; + kolab_files_selected = function() { var files = []; @@ -369,6 +380,11 @@ kolab_files_selected = function() return files; }; + +/***********************************************************/ +/********** Commands **********/ +/***********************************************************/ + rcube_webmail.prototype.files_sort = function(props) { var params = {}, @@ -507,7 +523,7 @@ function kolab_files_ui() $.each(this.env.folders, function(i, f) { var row = $('
  • '); - row.attr('id', f.id) + row.attr('id', f.id).data('folder', i) .append($('').text(f.name)) .click(function() { file_api.folder_select(i); }); @@ -516,6 +532,15 @@ function kolab_files_ui() if (f.virtual) row.addClass('virtual'); + else + row.mouseenter(function() { + if (rcmail.file_list.drag_active) + $(this).addClass('droptarget'); + }) + .mouseleave(function() { + if (rcmail.file_list.drag_active) + $(this).removeClass('droptarget'); + }); list.append(row); @@ -545,6 +570,46 @@ function kolab_files_ui() this.file_list(); }; + // folder create request + this.folder_create = function(folder) + { + this.req = this.set_busy(true, 'kolab_files.foldercreating'); + this.get('folder_create', {folder: folder}, 'folder_create_response'); + }; + + // folder create response handler + this.folder_create_response = function(response) + { + if (!this.response(response)) + return; + + this.display_message('kolab_files.foldercreatenotice', 'confirmation'); + + // refresh folders list + this.folder_list(); + }; + + // folder delete request + this.folder_delete = function(folder) + { + this.req = this.set_busy(true, 'kolab_files.folderdeleting'); + this.get('folder_delete', {folder: folder}, 'folder_delete_response'); + }; + + // folder delete response handler + this.folder_delete_response = function(response) + { + if (!this.response(response)) + return; + + this.env.folder = null; + rcmail.enable_command('files-folder-delete', 'files-folder-rename', false); + this.display_message('kolab_files.folderdeletenotice', 'confirmation'); + + // refresh folders list + this.folder_list(); + }; + this.file_list = function(params) { if (!this.env.folder || !rcmail.gui_objects.filelist) @@ -622,46 +687,6 @@ function kolab_files_ui() $(row).addClass('selected'); }; - // folder create request - this.folder_create = function(folder) - { - this.req = this.set_busy(true, 'kolab_files.foldercreating'); - this.get('folder_create', {folder: folder}, 'folder_create_response'); - }; - - // folder create response handler - this.folder_create_response = function(response) - { - if (!this.response(response)) - return; - - this.display_message('kolab_files.foldercreatenotice', 'confirmation'); - - // refresh folders list - this.folder_list(); - }; - - // folder delete request - this.folder_delete = function(folder) - { - this.req = this.set_busy(true, 'kolab_files.folderdeleting'); - this.get('folder_delete', {folder: folder}, 'folder_delete_response'); - }; - - // folder delete response handler - this.folder_delete_response = function(response) - { - if (!this.response(response)) - return; - - this.env.folder = null; - rcmail.enable_command('files-folder-delete', 'files-folder-rename', false); - this.display_message('kolab_files.folderdeletenotice', 'confirmation'); - - // refresh folders list - this.folder_list(); - }; - this.file_search = function(value) { if (value) { @@ -708,6 +733,31 @@ function kolab_files_ui() this.file_list(); }; + // file(s) move request + this.file_move = function(files, folder) + { + if (!files || !files.length || !folder) + return; + + var list = {}; + $.each(files, function(i, v) { + list[v] = folder + file_api.env.directory_separator + file_api.file_name(v); + }); + + this.req = this.set_busy(true, 'kolab_files.filemoving'); + this.get('file_move', {file: list}, 'file_move_response'); + }; + + // file(s) move response handler + this.file_move_response = function(response) + { + if (!this.response(response)) + return; + + this.display_message('kolab_files.filemovenotice', 'confirmation'); + this.file_list(); + }; + // file upload request this.file_upload = function(form) { diff --git a/plugins/kolab_files/lib/kolab_files_engine.php b/plugins/kolab_files/lib/kolab_files_engine.php index 2e109abc..4a87080e 100644 --- a/plugins/kolab_files/lib/kolab_files_engine.php +++ b/plugins/kolab_files/lib/kolab_files_engine.php @@ -462,7 +462,8 @@ class kolab_files_engine $this->rc->output->add_label('deletefolderconfirm', 'kolab_files.folderdeleting', 'kolab_files.foldercreating', 'kolab_files.uploading', 'kolab_files.filedeleteconfirm', - 'kolab_files.folderdeleteconfirm', 'kolab_files.filedeleting'); + 'kolab_files.folderdeleteconfirm', 'kolab_files.filedeleting', 'kolab_files.filedeletenotice', + 'kolab_files.filemoving', 'kolab_files.filemovenotice'); $this->rc->output->set_pagetitle($this->plugin->gettext('files')); $this->rc->output->send('kolab_files.files'); diff --git a/plugins/kolab_files/localization/en_US.inc b/plugins/kolab_files/localization/en_US.inc index e453f5e6..f657cb56 100644 --- a/plugins/kolab_files/localization/en_US.inc +++ b/plugins/kolab_files/localization/en_US.inc @@ -33,8 +33,10 @@ $labels['foldercreatenotice'] = 'Folder created successfully.'; $labels['saveallnotice'] = 'Successfully saved $n file(s).'; $labels['saveallerror'] = 'Saving $n file(s) failed.'; $labels['attacherror'] = 'Failed to attach file(s) from the cloud'; +$labels['filemoving'] = 'Moving file(s)...'; $labels['filedeleting'] = 'Deleting file(s)...'; $labels['filedeleteconfirm'] = 'Are you sure you want to delete selected files?'; $labels['filedeletenotice'] = 'File(s) deleted successfully.'; +$labels['filemovenotice'] = 'File(s) moved successfully.'; ?> From 8ee53e5f1ce9fa407c5b4df0d57081bbba944ad0 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sun, 17 Mar 2013 11:57:26 +0100 Subject: [PATCH 6/6] Remove outline on hidden input in smart upload button --- plugins/kolab_files/skins/larry/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kolab_files/skins/larry/ui.js b/plugins/kolab_files/skins/larry/ui.js index 760aaf34..a5e359bd 100644 --- a/plugins/kolab_files/skins/larry/ui.js +++ b/plugins/kolab_files/skins/larry/ui.js @@ -103,7 +103,7 @@ function kolab_files_upload_input(button) file.attr({name: 'file[]', type: 'file', multiple: 'multiple', size: 5}) .change(function() { rcmail.files_upload('#filesuploadform'); }) // opacity:0 does the trick, display/visibility doesn't work - .css({opacity: 0, cursor: 'pointer', position: 'absolute', top: '10000px', left: '10000px'}); + .css({opacity: 0, cursor: 'pointer', outline: 'none', position: 'absolute', top: '10000px', left: '10000px'}); // In FF and IE we need to move the browser file-input's button under the cursor // Thanks to the size attribute above we know the length of the input field