33 commits - plugins/calendar plugins/kolab_addressbook plugins/kolab_auth plugins/kolab_delegation plugins/libcalendaring plugins/libkolab plugins/tasklist
Thomas Brüderli
bruederli at kolabsys.com
Fri May 23 14:30:25 CEST 2014
plugins/calendar/calendar.php | 26
plugins/calendar/calendar_ui.js | 165 ++-
plugins/calendar/drivers/calendar_driver.php | 9
plugins/calendar/drivers/database/database_driver.php | 15
plugins/calendar/drivers/kolab/kolab_calendar.php | 117 +-
plugins/calendar/drivers/kolab/kolab_driver.php | 227 +++-
plugins/calendar/drivers/kolab/kolab_user_calendar.php | 396 ++++++++
plugins/calendar/lib/calendar_ui.php | 188 ++-
plugins/calendar/localization/en_US.inc | 3
plugins/calendar/skins/classic/calendar.css | 141 ++
plugins/calendar/skins/classic/images/calendars.gif |binary
plugins/calendar/skins/classic/images/calendars.png |binary
plugins/calendar/skins/classic/templates/calendar.html | 9
plugins/calendar/skins/larry/calendar.css | 174 ++-
plugins/calendar/skins/larry/images/calendars.png |binary
plugins/calendar/skins/larry/templates/calendar.html | 175 +++
plugins/kolab_addressbook/kolab_addressbook.php | 2
plugins/kolab_auth/kolab_auth_ldap.php | 7
plugins/kolab_delegation/kolab_delegation_engine.php | 6
plugins/libcalendaring/libvcalendar.php | 5
plugins/libkolab/config.inc.php.dist | 21
plugins/libkolab/js/folderlist.js | 225 ++++
plugins/libkolab/lib/kolab_storage.php | 473 ++++++++--
plugins/libkolab/lib/kolab_storage_folder.php | 170 ---
plugins/libkolab/lib/kolab_storage_folder_api.php | 298 ++++++
plugins/libkolab/lib/kolab_storage_folder_user.php | 101 ++
plugins/libkolab/lib/kolab_storage_folder_virtual.php | 59 +
plugins/tasklist/drivers/database/tasklist_database_driver.php | 12
plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php | 313 +++++-
plugins/tasklist/drivers/tasklist_driver.php | 9
plugins/tasklist/localization/en_US.inc | 3
plugins/tasklist/skins/larry/sprites.png |binary
plugins/tasklist/skins/larry/tasklist.css | 145 ++-
plugins/tasklist/skins/larry/templates/mainview.html | 37
plugins/tasklist/tasklist.js | 157 ++-
plugins/tasklist/tasklist.php | 50 +
plugins/tasklist/tasklist_ui.php | 127 ++
37 files changed, 3151 insertions(+), 714 deletions(-)
New commits:
commit 5763fb81e817a1d58cbe57033defa438840165cd
Merge: 69d9d3b 6454bb5
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri May 23 10:12:29 2014 +0200
Merge new folder navigation from branch 'dev/new-foldernav'
diff --cc plugins/tasklist/skins/larry/tasklist.css
index f8d9d0f,93e0b2b..54372fa
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@@ -74,8 -74,34 +74,34 @@@ body.attachmentwin #topnav .topright
bottom: 0px;
}
+ #tasklistsbox .boxtitle a.iconbutton.search {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 16px;
+ cursor: pointer;
+ background-position: -2px -317px;
+ }
+
+ #tasklistsbox .listsearchbox {
+ display: none;
+ }
+
+ #tasklistsbox .listsearchbox.expanded {
+ display: block;
+ }
+
+ #tasklistsbox .scroller {
+ top: 34px;
+ }
+
+ #tasklistsbox .listsearchbox.expanded + .scroller {
+ top: 68px;
+ }
+
+
#taskselector {
- margin: -4px 40px 0 0;
+ margin: -1px 40px 0 0;
padding: 0;
}
diff --cc plugins/tasklist/tasklist_ui.php
index f6d4b3a,05acafd..377fe68
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@@ -102,48 -170,36 +170,51 @@@ class tasklist_u
$prop['undelete'] = $this->plugin->driver->undelete;
$prop['sortable'] = $this->plugin->driver->sortable;
$prop['attachments'] = $this->plugin->driver->attachments;
-
- if (!$prop['virtual'])
- $jsenv[$id] = $prop;
-
- $html_id = html_identifier($id);
- $class = 'tasks-' . asciiwords($id, true);
- $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
-
- if ($prop['virtual'])
- $class .= ' virtual';
- else if (!$prop['editable'])
- $class .= ' readonly';
- if ($prop['class_name'])
- $class .= ' '.$prop['class_name'];
-
- $li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class),
- ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active']))) .
- html::span(array('class' => 'handle', 'title' => $this->plugin->gettext('focusview')), ' ') .
- html::span(array('class' => 'listname', 'title' => $title), $prop['listname']));
+ $jsenv[$id] = $prop;
}
- $this->rc->output->set_env('tasklists', $jsenv);
- $this->rc->output->add_gui_object('folderlist', $attrib['id']);
+ $classes = array('tasklist');
+ $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+ html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
+
+ if ($prop['virtual'])
+ $classes[] = 'virtual';
+ else if (!$prop['editable'])
+ $classes[] = 'readonly';
+ if ($prop['subscribed'])
+ $classes[] = 'subscribed';
+ if ($prop['class'])
+ $classes[] = $prop['class'];
+
+ if (!$activeonly || $prop['active']) {
+ return html::div(join(' ', $classes),
+ html::span(array('class' => 'listname', 'title' => $title), $prop['listname'] ?: $prop['name']) .
+ ($prop['virtual'] ? '' :
+ html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
+ html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), ' ') .
+ (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe')), ' ') : '')
+ )
+ );
+ }
- return html::tag('ul', $attrib, $li, html::$common_attrib);
+ return '';
}
+ /**
+ * Render HTML form for task status selector
+ */
+ function status_select($attrib = array())
+ {
+ $attrib['name'] = 'status';
+ $select = new html_select($attrib);
+ $select->add('---', '');
+ $select->add($this->plugin->gettext('status-needs-action'), 'NEEDS-ACTION');
+ $select->add($this->plugin->gettext('status-in-process'), 'IN-PROCESS');
+ $select->add($this->plugin->gettext('status-completed'), 'COMPLETED');
+ $select->add($this->plugin->gettext('status-cancelled'), 'CANCELLED');
+
+ return $select->show(null);
+ }
/**
* Render a HTML select box for list selection
commit 6454bb57cfbf59846eb5f6f8f80a620e95f73682
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri May 23 10:08:20 2014 +0200
Small fix when listing subscribed user folders
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index dd61513..28c22e9 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -792,8 +792,9 @@ class kolab_storage
$folders = self::$imap->list_folders_subscribed($root, $mbox);
// add temporarily subscribed folders
- if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders']))
+ if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
$folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+ }
}
else {
$folders = self::_imap_list_folders($root, $mbox);
@@ -1494,12 +1495,18 @@ class kolab_storage
$other_ns = rtrim(self::namespace_root('other'), $delimiter);
$path_len = count(explode($delimiter, $other_ns));
- foreach ((array)self::list_folders($other_ns . $delimiter, '*', $type, $subscribed) as $foldername) {
+ foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
if ($foldername == 'INBOX') // skip INBOX which is added by default
continue;
- // truncate folder path to top-level folders of the 'other' namespace
$path = explode($delimiter, $foldername);
+
+ // compare folder type if a subfolder is listed
+ if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
+ continue;
+ }
+
+ // truncate folder path to top-level folders of the 'other' namespace
$foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
if (!$folders[$foldername]) {
commit 5079da4a90dc69a6de4d8a674fa6c5612c7d743a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri May 23 09:32:43 2014 +0200
Don't fetch all annotations from 'other' namespace when called from kolab_storage::list_user_folders()
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 609ac5e..dd61513 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -760,12 +760,17 @@ class kolab_storage
return $folders;
}
-
$prefix = $root . $mbox;
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
- // get folders types
- $folderdata = self::folders_typedata($prefix);
+ // get folders types for all folders
+ if (!$subscribed || $prefix == '*' || !self::$config->get('kolab_skip_namespace')) {
+ $folderdata = self::folders_typedata($prefix);
+ }
+ else {
+ // fetch folder types for the effective list of (subscribed) folders when post-filtering
+ $folderdata = array();
+ }
if (!is_array($folderdata)) {
return array();
@@ -1489,7 +1494,7 @@ class kolab_storage
$other_ns = rtrim(self::namespace_root('other'), $delimiter);
$path_len = count(explode($delimiter, $other_ns));
- foreach ((array)self::list_folders($other_ns, '*', $type, $subscribed) as $foldername) {
+ foreach ((array)self::list_folders($other_ns . $delimiter, '*', $type, $subscribed) as $foldername) {
if ($foldername == 'INBOX') // skip INBOX which is added by default
continue;
commit bb0312280cc72e2a5b48a08ba85b6a6282f54630
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri May 23 09:18:35 2014 +0200
Avoid PHP errors when IMAP doesn't provide valid data
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 1fd3d58..609ac5e 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1023,14 +1023,15 @@ class kolab_storage
// GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
if ($ns_root == '') {
foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
- if (!in_array($folder, $blacklist)) {
+ if (!in_array($folder, $blacklist) &&
+ ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
$folderdata[$folder] = $metadata;
- $folderdata += self::$imap->get_metadata($folder.$delimiter.'*', $type_keys);
+ $folderdata += $data;
}
}
}
- else {
- $folderdata += self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys);
+ else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
+ $folderdata += $data;
}
}
}
@@ -1324,7 +1325,7 @@ class kolab_storage
// check if we have any folder in personal namespace
// folder(s) may exist but not subscribed
- foreach ($folders as $f => $data) {
+ foreach ((array)$folders as $f => $data) {
if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
$folder = $f;
break;
commit d9247aa5e55259c20dc474bd8ab157d3b6bf2724
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri May 23 09:00:46 2014 +0200
Add config option to exclude certain namespaces from grouware folder listing
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index a95e1c2..4df2d85 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -79,7 +79,7 @@ class kolab_driver extends calendar_driver
return $this->calendars;
// get all folders that have "event" type, sorted by namespace/name
- $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders(true));
+ $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders('event', true));
$this->calendars = array();
foreach ($folders as $folder) {
diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist
index 3a3c287..fd8ac84 100644
--- a/plugins/libkolab/config.inc.php.dist
+++ b/plugins/libkolab/config.inc.php.dist
@@ -16,6 +16,10 @@ $rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
// folders in calendar view or available addressbooks
$rcmail_config['kolab_use_subscriptions'] = false;
+// List any of 'personal','shared','other' namespaces to be excluded from groupware folder listing
+// example: array('other');
+$rcmail_config['kolab_skip_namespace'] = null;
+
// Enables the use of displayname folder annotations as introduced in KEP:?
// for displaying resource folder names (experimental!)
$rcmail_config['kolab_custom_display_names'] = false;
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 4266fb5..1fd3d58 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -40,7 +40,9 @@ class kolab_storage
public static $encode_ids = false;
private static $ready = false;
+ private static $with_tempsubs = true;
private static $subscriptions;
+ private static $typedata = array();
private static $states;
private static $config;
private static $imap;
@@ -313,7 +315,7 @@ class kolab_storage
}
}
- return '/';
+ return '';
}
@@ -748,11 +750,12 @@ class kolab_storage
if ($subscribed) {
$folders = self::$imap->list_folders_subscribed($root, $mbox);
// add temporarily subscribed folders
- if (is_array($_SESSION['kolab_subscribed_folders']))
+ if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
$folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+ }
}
else {
- $folders = self::$imap->list_folders($root, $mbox);
+ $folders = self::_imap_list_folders($root, $mbox);
}
return $folders;
@@ -784,11 +787,11 @@ class kolab_storage
$folders = self::$imap->list_folders_subscribed($root, $mbox);
// add temporarily subscribed folders
- if (is_array($_SESSION['kolab_subscribed_folders']))
+ if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders']))
$folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
}
else {
- $folders = self::$imap->list_folders($root, $mbox);
+ $folders = self::_imap_list_folders($root, $mbox);
}
// In case of an error, return empty list (?)
@@ -798,6 +801,11 @@ class kolab_storage
// Filter folders list
foreach ($folders as $idx => $folder) {
+ // lookup folder type
+ if (!array_key_exists($folder, $folderdata)) {
+ $folderdata[$folder] = self::folder_type($folder);
+ }
+
$type = $folderdata[$folder];
if ($filter == 'mail' && empty($type)) {
@@ -811,6 +819,38 @@ class kolab_storage
return $folders;
}
+ /**
+ * Wrapper for rcube_imap::list_folders() with optional post-filtering
+ */
+ protected static function _imap_list_folders($root, $mbox)
+ {
+ $postfilter = null;
+
+ // compose a post-filter expression for the excluded namespaces
+ if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+ $excludes = array();
+ foreach ((array)$skip_ns as $ns) {
+ if ($ns_root = self::namespace_root($ns)) {
+ $excludes[] = $ns_root;
+ }
+ }
+
+ if (count($excludes)) {
+ $postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
+ }
+ }
+
+ // use normal LIST command to return all folders, it's fast enough
+ $folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
+
+ if (!empty($postfilter)) {
+ $folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
+ $folders = self::$imap->sort_folder_list($folders);
+ }
+
+ return $folders;
+ }
+
/**
* Search for shared or otherwise not listed groupware folders the user has access
@@ -959,13 +999,54 @@ class kolab_storage
return false;
}
- $folderdata = self::$imap->get_metadata($prefix, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
+ // return cached result
+ if (is_array(self::$typedata[$prefix])) {
+ return self::$typedata[$prefix];
+ }
+
+ $type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
+
+ // fetch metadata from *some* folders only
+ if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+ $delimiter = self::$imap->get_hierarchy_delimiter();
+ $folderdata = $blacklist = array();
+ foreach ((array)$skip_ns as $ns) {
+ if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
+ $blacklist[] = $ns_root;
+ }
+ }
+ foreach (array('personal','other','shared') as $ns) {
+ if (!in_array($ns, (array)$skip_ns)) {
+ $ns_root = rtrim(self::namespace_root($ns), $delimiter);
+
+ // list top-level folders and their childs one by one
+ // GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
+ if ($ns_root == '') {
+ foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
+ if (!in_array($folder, $blacklist)) {
+ $folderdata[$folder] = $metadata;
+ $folderdata += self::$imap->get_metadata($folder.$delimiter.'*', $type_keys);
+ }
+ }
+ }
+ else {
+ $folderdata += self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys);
+ }
+ }
+ }
+ }
+ else {
+ $folderdata = self::$imap->get_metadata($prefix, $type_keys);
+ }
if (!is_array($folderdata)) {
return false;
}
- return array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+ // keep list in memory
+ self::$typedata[$prefix] = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+
+ return self::$typedata[$prefix];
}
@@ -996,6 +1077,11 @@ class kolab_storage
{
self::setup();
+ // return in-memory cached result
+ if (is_array(self::$typedata['*']) && array_key_exists($folder, self::$typedata['*'])) {
+ return self::$typedata['*'][$folder];
+ }
+
$metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($metadata)) {
@@ -1045,7 +1131,9 @@ class kolab_storage
{
if (self::$subscriptions === null) {
self::setup();
+ self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
+ self::$with_tempsubs = true;
}
return in_array($folder, self::$subscriptions) ||
@@ -1177,7 +1265,9 @@ class kolab_storage
else {
self::setup();
if (self::$subscriptions === null) {
+ self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
+ self::$with_tempsubs = true;
}
self::$states = self::$subscriptions;
$folders = implode(self::$states, '**');
@@ -1372,8 +1462,9 @@ class kolab_storage
if (!empty($user[$user_attrib])) {
list($mbox) = explode('@', $user[$user_attrib]);
+ $delimiter = self::$imap->get_hierarchy_delimiter();
$other_ns = self::namespace_root('other');
- $folders = self::list_folders($other_ns . $mbox, '*', $type, $subscribed, $folderdata);
+ $folders = self::list_folders($other_ns . $mbox . $delimiter, '*', $type, $subscribed, $folderdata);
}
return $folders;
@@ -1383,11 +1474,12 @@ class kolab_storage
/**
* Get a list of (virtual) top-level folders from the other users namespace
*
+ * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of kolab_storage_folder_user objects
*/
- public static function get_user_folders($subscribed)
+ public static function get_user_folders($type, $subscribed)
{
$folders = $folderdata = array();
@@ -1396,7 +1488,7 @@ class kolab_storage
$other_ns = rtrim(self::namespace_root('other'), $delimiter);
$path_len = count(explode($delimiter, $other_ns));
- foreach ((array)self::list_folders($other_ns, '*', '', $subscribed) as $foldername) {
+ foreach ((array)self::list_folders($other_ns, '*', $type, $subscribed) as $foldername) {
if ($foldername == 'INBOX') // skip INBOX which is added by default
continue;
commit 95491f0886228da853fd48257e1dfabc400bbea1
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu May 22 19:00:49 2014 +0200
Don't write session data when fetching events/tasks
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 3e5d28a..79df28a 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -302,6 +302,9 @@ class kolab_calendar extends kolab_storage_folder_api
}
}
+ // avoid session race conditions that will loose temporary subscriptions
+ $this->cal->rc->session->nowrite();
+
return $events;
}
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index 1ab8679..dc9bcf7 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -222,6 +222,9 @@ class kolab_user_calendar extends kolab_calendar
}
}
+ // avoid session race conditions that will loose temporary subscriptions
+ $this->cal->rc->session->nowrite();
+
return $events;
}
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 50fa371..90c3048 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -481,6 +481,9 @@ class tasklist_kolab_driver extends tasklist_driver
}
}
+ // avoid session race conditions that will loose temporary subscriptions
+ $this->plugin->rc->session->nowrite();
+
return $counts;
}
@@ -537,6 +540,9 @@ class tasklist_kolab_driver extends tasklist_driver
}
}
+ // avoid session race conditions that will loose temporary subscriptions
+ $this->plugin->rc->session->nowrite();
+
return $results;
}
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 75ba386..76d50d5 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -164,7 +164,7 @@ function rcube_tasklist_ui(settings)
var prop = { id:p.id, active:list.active?1:0 };
if (list.subscribed) prop.permanent = 1;
rcmail.http_post('tasklist', { action:'subscribe', l:prop });
- setTimeout(function(){ list_tasks(); }, 500);
+ list_tasks();
}
});
commit 6c0985dfe9104d53bca05e6dc797b3c50f59183e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed May 21 19:49:34 2014 +0200
Avoid warnings on empty message lists
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 15c81f9..154b0a3 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2110,7 +2110,7 @@ class calendar extends rcube_plugin
*/
public function mail_messages_list($p)
{
- if (in_array('attachment', (array)$p['cols'])) {
+ if (in_array('attachment', (array)$p['cols']) && !empty($p['messages'])) {
foreach ($p['messages'] as $i => $header) {
$part = new StdClass;
$part->mimetype = $header->ctype;
commit 63993e3a7d82967059b2f176537bbe6d1ee0b4d7
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed May 21 14:03:18 2014 +0200
Additional styles for the tasklist search results to match the calendar view
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index f33f525..93e0b2b 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -384,6 +384,20 @@ body.attachmentwin #topnav .topright {
top: 6px;
}
+#tasklistsbox .searchresults {
+ background: #b0ccd7;
+ margin-top: 8px;
+}
+
+#tasklistsbox .searchresults .boxtitle {
+ background: none;
+ padding: 2px 8px 2px 8px;
+}
+
+#tasklistsbox .searchresults .listing li {
+ background-color: #c7e3ef;
+}
+
#mainview-right {
position: absolute;
top: 0;
commit 8c507885c52c7b2fb8212f337960cbd31fbb1c84
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed May 21 13:56:44 2014 +0200
Fix list creation/update/deletion (#3047)
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 52d2337..50fa371 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -139,7 +139,7 @@ class tasklist_kolab_driver extends tasklist_driver
'virtual' => $folder->virtual,
'children' => true, // TODO: determine if that folder indeed has child folders
'subscribed' => (bool)$folder->is_subscribed(),
- 'group' => $folder->get_namespace(),
+ 'group' => $folder->default ? 'default' : $folder->get_namespace(),
'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
);
}
@@ -284,8 +284,13 @@ class tasklist_kolab_driver extends tasklist_driver
$this->rc->user->save_prefs($prefs);
// force page reload to properly render folder hierarchy
- if (!empty($prop['parent']))
+ if (!empty($prop['parent'])) {
$prop['_reload'] = true;
+ }
+ else {
+ $folder = kolab_storage::get_folder($folder);
+ $prop += $this->folder_props($folder, $this->rc->get_storage()->get_hierarchy_delimiter(), array());
+ }
return $id;
}
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 774e9b4..75ba386 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -164,7 +164,7 @@ function rcube_tasklist_ui(settings)
var prop = { id:p.id, active:list.active?1:0 };
if (list.subscribed) prop.permanent = 1;
rcmail.http_post('tasklist', { action:'subscribe', l:prop });
- list_tasks();
+ setTimeout(function(){ list_tasks(); }, 500);
}
});
@@ -737,10 +737,11 @@ function rcube_tasklist_ui(settings)
var tag = draggable.data('value'),
drop_id = $(this).data('id'),
- drop_rec = listdata[drop_id];
+ drop_rec = listdata[drop_id],
+ list = drop_rec && me.tasklists[drop_rec.list] ? me.tasklists[drop_rec.list] : { editable:true };
- // target already has this tag assigned
- if (!drop_rec || (drop_rec.tags && $.inArray(tag, drop_rec.tags) >= 0)) {
+ // target is not writeable or already has this tag assigned
+ if (!drop_rec || drop_rec.readonly || !list.editable || (drop_rec.tags && $.inArray(tag, drop_rec.tags) >= 0)) {
return false;
}
@@ -1048,7 +1049,7 @@ function rcube_tasklist_ui(settings)
function task_draggable_start(event, ui)
{
- $('.taskhead, #rootdroppable, #'+rcmail.gui_objects.folderlist.id+' li').droppable({
+ $('.taskhead, #rootdroppable, #'+rcmail.gui_objects.tasklistslist.id+' li').droppable({
hoverClass: 'droptarget',
accept: task_droppable_accept,
drop: task_draggable_dropped,
@@ -1187,7 +1188,7 @@ function rcube_tasklist_ui(settings)
*/
function task_show_dialog(id)
{
- var $dialog = $('#taskshow'), rec;
+ var $dialog = $('#taskshow'), rec, list;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
@@ -1196,6 +1197,7 @@ function rcube_tasklist_ui(settings)
return;
me.selected_task = rec;
+ list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {};
// fill dialog data
$('#task-parent-title').html(Q(rec.parent_title || '')+' »').css('display', rec.parent_title ? 'block' : 'none');
@@ -1247,22 +1249,24 @@ function rcube_tasklist_ui(settings)
// define dialog buttons
var buttons = [];
- buttons.push({
- text: rcmail.gettext('edit','tasklist'),
- click: function() {
- task_edit_dialog(me.selected_task.id, 'edit');
- },
- disabled: rcmail.busy
- });
+ if (list.editable && !rec.readonly) {
+ buttons.push({
+ text: rcmail.gettext('edit','tasklist'),
+ click: function() {
+ task_edit_dialog(me.selected_task.id, 'edit');
+ },
+ disabled: rcmail.busy
+ });
- buttons.push({
- text: rcmail.gettext('delete','tasklist'),
- click: function() {
- if (delete_task(me.selected_task.id))
- $dialog.dialog('close');
- },
- disabled: rcmail.busy
- });
+ buttons.push({
+ text: rcmail.gettext('delete','tasklist'),
+ click: function() {
+ if (delete_task(me.selected_task.id))
+ $dialog.dialog('close');
+ },
+ disabled: rcmail.busy
+ });
+ }
// open jquery UI dialog
$dialog.dialog({
@@ -1828,15 +1832,12 @@ function rcube_tasklist_ui(settings)
delete_ids.push(prop.id);
}
- // delete all calendars in the list
+ // delete all subfolders in the list
for (var i=0; i < delete_ids.length; i++) {
id = delete_ids[i];
list = me.tasklists[id];
- li = rcmail.get_folder_li(id, 'rcmlitasklist');
+ tasklists_widget.remove(id);
- if (li) {
- $(li).remove();
- }
if (list) {
list.active = false;
// delete me.tasklists[prop.id];
@@ -1857,13 +1858,15 @@ function rcube_tasklist_ui(settings)
return;
}
- var li = $('<li>').attr('id', 'rcmlitasklist'+prop.id)
- .append('<input type="checkbox" name="_list[]" value="'+prop.id+'" checked="checked" />')
- .append('<span class="handle"> </span>')
- .append('<span class="listname">'+Q(prop.name)+'</span>');
- $(rcmail.gui_objects.folderlist).append(li);
+ tasklists_widget.insert({
+ id: prop.id,
+ classes: [ prop.group || '' ],
+ virtual: prop.virtual,
+ html: prop.html
+ }, prop.parent || null, prop.group);
+
+ delete prop.html;
me.tasklists[prop.id] = prop;
- init_tasklist_li(li.get(0), prop.id);
// append to list selector in task edit dialog, too (#2985)
$('<option>').attr('value', prop.id).html(Q(prop.name)).appendTo('#taskedit-tasklist');
@@ -1875,7 +1878,7 @@ function rcube_tasklist_ui(settings)
function update_list(prop)
{
var id = prop.oldid || prop.id,
- li = rcmail.get_folder_li(id, 'rcmlitasklist');
+ li = tasklists_widget.get_item(id);
if (prop._reload) {
rcmail.redirect(rcmail.url(''));
@@ -1885,10 +1888,9 @@ function rcube_tasklist_ui(settings)
if (me.tasklists[id] && li) {
delete me.tasklists[id];
me.tasklists[prop.id] = prop;
- $(li).data('id', prop.id)
- .attr('id', 'rcmlitasklist'+prop.id)
- .find('input').data('id', prop.id);
- $('.listname', li).html(Q(prop.name));
+ $(li).find('input').first().val(prop.id);
+ $(li).find('.listname').first().html(Q(prop.name));
+ tasklists_widget.update(id, { id:prop.id, html:li.children().first() });
}
}
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 1a70699..3004435 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -607,6 +607,11 @@ class tasklist extends rcube_plugin
$list += array('showalarms' => true, 'active' => true, 'editable' => true);
if ($insert_id = $this->driver->create_list($list)) {
$list['id'] = $insert_id;
+ if (!$list['_reload']) {
+ $this->load_ui();
+ $list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv);
+ $list += (array)$jsenv[$insert_id];
+ }
$this->rc->output->command('plugin.insert_tasklist', $list);
$success = true;
}
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 6f92d26..05acafd 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -188,7 +188,7 @@ class tasklist_ui
if (!$activeonly || $prop['active']) {
return html::div(join(' ', $classes),
- html::span(array('class' => 'listname', 'title' => $title), $prop['listname']) .
+ html::span(array('class' => 'listname', 'title' => $title), $prop['listname'] ?: $prop['name']) .
($prop['virtual'] ? '' :
html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), ' ') .
commit f4f5a30e0ae0d6e0c4e4b6d04c8d64174d0bf53d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed May 21 13:04:18 2014 +0200
Add new folder navigation to tasks module (#3047)
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 70e0c37..edcca42 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -119,8 +119,8 @@ class calendar_ui
$this->rc->output->include_script('treelist.js');
// include kolab folderlist widget if available
- if (is_readable($this->cal->home . '/lib/js/folderlist.js')) {
- $this->cal->include_script('lib/js/folderlist.js');
+ if (is_readable($this->cal->api->dir . 'libkolab/js/folderlist.js')) {
+ $this->cal->api->include_script('libkolab/js/folderlist.js');
}
jqueryui::miniColors();
diff --git a/plugins/calendar/lib/js/folderlist.js b/plugins/calendar/lib/js/folderlist.js
deleted file mode 120000
index c49706b..0000000
--- a/plugins/calendar/lib/js/folderlist.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../libkolab/js/folderlist.js
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_folder_api.php b/plugins/libkolab/lib/kolab_storage_folder_api.php
index 5af8c34..4a90467 100644
--- a/plugins/libkolab/lib/kolab_storage_folder_api.php
+++ b/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -149,6 +149,24 @@ abstract class kolab_storage_folder_api
return rcube_charset::convert(end($parts), 'UTF7-IMAP');
}
+ /**
+ * Getter for parent folder path
+ *
+ * @return string Full path to parent folder
+ */
+ public function get_parent()
+ {
+ $path = explode('/', $this->name);
+ array_pop($path);
+
+ // don't list top-level namespace folder
+ if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
+ $path = array();
+ }
+
+ return join('/', $path);
+ }
+
/**
* Get the color value stored in metadata
diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
index d9bf414..cab4fa7 100644
--- a/plugins/tasklist/drivers/database/tasklist_database_driver.php
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -200,6 +200,18 @@ class tasklist_database_driver extends tasklist_driver
}
/**
+ * Search for shared or otherwise not listed tasklists the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of tasklists
+ */
+ public function search_lists($query, $source)
+ {
+ return array();
+ }
+
+ /**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index ad36777..52d2337 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -45,12 +45,15 @@ class tasklist_kolab_driver extends tasklist_driver
$this->rc = $plugin->rc;
$this->plugin = $plugin;
- $this->_read_lists();
-
if (kolab_storage::$version == '2.0') {
$this->alarm_absolute = false;
}
+ // tasklist use fully encoded identifiers
+ kolab_storage::$encode_ids = true;
+
+ $this->_read_lists();
+
$this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
}
@@ -83,87 +86,171 @@ class tasklist_kolab_driver extends tasklist_driver
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$prefs = $this->rc->config->get('kolab_tasklists', array());
- $listnames = array();
+
+ foreach ($folders as $folder) {
+ $tasklist = $this->folder_props($folder, $delim, $prefs);
+
+ $this->lists[$tasklist['id']] = $tasklist;
+ $this->folders[$tasklist['id']] = $folder;
+ $this->folders[$folder->name] = $folder;
+ }
+ }
+
+ /**
+ * Derive list properties from the given kolab_storage_folder object
+ */
+ protected function folder_props($folder, $delim, $prefs)
+ {
+ if ($folder->get_namespace() == 'personal') {
+ $norename = false;
+ $readonly = false;
+ $alarms = true;
+ }
+ else {
+ $alarms = false;
+ $readonly = true;
+ if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
+ if (strpos($rights, 'i') !== false)
+ $readonly = false;
+ }
+ $info = $folder->get_folder_info();
+ $norename = $readonly || $info['norename'] || $info['protected'];
+ }
+
+ $list_id = $folder->id; #kolab_storage::folder_id($folder->name);
+ $old_id = kolab_storage::folder_id($folder->name, false);
+
+ if (!isset($prefs[$list_id]['showalarms']) && isset($prefs[$old_id]['showalarms'])) {
+ $prefs[$list_id]['showalarms'] = $prefs[$old_id]['showalarms'];
+ }
+
+ return array(
+ 'id' => $list_id,
+ 'name' => $folder->get_name(),
+ 'listname' => $folder->get_foldername(),
+ 'editname' => $folder->get_foldername(),
+ 'color' => $folder->get_color('0000CC'),
+ 'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
+ 'editable' => !$readonly,
+ 'norename' => $norename,
+ 'active' => $folder->is_active(),
+ 'parentfolder' => $folder->get_parent(),
+ 'default' => $folder->default,
+ 'virtual' => $folder->virtual,
+ 'children' => true, // TODO: determine if that folder indeed has child folders
+ 'subscribed' => (bool)$folder->is_subscribed(),
+ 'group' => $folder->get_namespace(),
+ 'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
+ );
+ }
+
+ /**
+ * Get a list of available task lists from this source
+ */
+ public function get_lists(&$tree = null)
+ {
+ // attempt to create a default list for this user
+ if (empty($this->lists)) {
+ $prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true);
+ if ($this->create_list($prop))
+ $this->_read_lists(true);
+ }
+
+ $folders = array();
+ foreach ($this->lists as $id => $list) {
+ if (!empty($this->folders[$id])) {
+ $folders[] = $this->folders[$id];
+ }
+ }
// include virtual folders for a full folder tree
- if (!$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
- $folders = kolab_storage::folder_hierarchy($folders);
+ if (!is_null($tree)) {
+ $folders = kolab_storage::folder_hierarchy($folders, $tree);
+ }
+ $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
+ $prefs = $this->rc->config->get('kolab_tasklists', array());
+
+ $lists = array();
foreach ($folders as $folder) {
- $utf7name = $folder->name;
+ $list_id = $folder->id; #kolab_storage::folder_id($folder->name);
+ $imap_path = explode($delim, $folder->name);
- $path_imap = explode($delim, $utf7name);
- $editname = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP'); // pop off raw name part
- $path_imap = join($delim, $path_imap);
+ // find parent
+ do {
+ array_pop($imap_path);
+ $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
+ }
+ while (count($imap_path) > 1 && !$this->folders[$parent_id]);
+
+ // restore "real" parent ID
+ if ($parent_id && !$this->folders[$parent_id]) {
+ $parent_id = kolab_storage::folder_id($folder->get_parent());
+ }
$fullname = $folder->get_name();
- $listname = kolab_storage::folder_displayname($fullname, $listnames);
+ $listname = $folder->get_foldername();
// special handling for virtual folders
- if ($folder->virtual) {
- $list_id = kolab_storage::folder_id($utf7name);
- $this->lists[$list_id] = array(
- 'id' => $list_id,
- 'name' => $fullname,
+ if ($folder instanceof kolab_storage_folder_user) {
+ $lists[$list_id] = array(
+ 'id' => $list_id,
+ 'name' => $folder->get_name(),
'listname' => $listname,
- 'virtual' => true,
+ 'title' => $folder->get_owner(),
+ 'virtual' => true,
'editable' => false,
+ 'group' => 'other virtual',
+ 'class' => 'user',
+ 'parent' => $parent_id,
);
- continue;
}
-
- if ($folder->get_namespace() == 'personal') {
- $norename = false;
- $readonly = false;
- $alarms = true;
+ else if ($folder->virtual) {
+ $lists[$list_id] = array(
+ 'id' => $list_id,
+ 'name' => kolab_storage::object_name($fullname),
+ 'listname' => $listname,
+ 'virtual' => true,
+ 'editable' => false,
+ 'group' => $folder->get_namespace(),
+ 'class' => 'folder',
+ 'parent' => $parent_id,
+ );
}
else {
- $alarms = false;
- $readonly = true;
- if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
- if (strpos($rights, 'i') !== false)
- $readonly = false;
+ if (!$this->lists[$list_id]) {
+ $this->lists[$list_id] = $this->folder_props($folder, $delim, $prefs);
+ $this->folders[$list_id] = $folder;
}
- $info = $folder->get_folder_info();
- $norename = $readonly || $info['norename'] || $info['protected'];
+ $this->lists[$list_id]['parent'] = $parent_id;
+ $lists[$list_id] = $this->lists[$list_id];
}
-
- $list_id = kolab_storage::folder_id($utf7name);
- $tasklist = array(
- 'id' => $list_id,
- 'name' => $fullname,
- 'listname' => $listname,
- 'editname' => $editname,
- 'color' => $folder->get_color('0000CC'),
- 'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
- 'editable' => !$readionly,
- 'norename' => $norename,
- 'active' => $folder->is_active(),
- 'parentfolder' => $path_imap,
- 'default' => $folder->default,
- 'children' => true, // TODO: determine if that folder indeed has child folders
- 'class_name' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
- );
- $this->lists[$tasklist['id']] = $tasklist;
- $this->folders[$tasklist['id']] = $folder;
- $this->folders[$folder->name] = $folder;
}
+
+ return $lists;
}
/**
- * Get a list of available task lists from this source
+ * Get the kolab_calendar instance for the given calendar ID
+ *
+ * @param string List identifier (encoded imap folder name)
+ * @return object kolab_storage_folder Object nor null if list doesn't exist
*/
- public function get_lists()
+ protected function get_folder($id)
{
- // attempt to create a default list for this user
- if (empty($this->lists)) {
- if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true)))
- $this->_read_lists(true);
+ // create list and folder instance if necesary
+ if (!$this->lists[$id]) {
+ $folder = kolab_storage::get_folder(kolab_storage::id_decode($id));
+ if ($folder->type) {
+ $this->folders[$id] = $folder;
+ $this->lists[$id] = $this->folder_props($folder, $this->rc->get_storage()->get_hierarchy_delimiter(), $this->rc->config->get('kolab_tasklists', array()));
+ }
}
- return $this->lists;
+ return $this->folders[$id];
}
+
/**
* Create a new list assigned to the current user
*
@@ -215,7 +302,7 @@ class tasklist_kolab_driver extends tasklist_driver
*/
public function edit_list(&$prop)
{
- if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
+ if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
$prop['oldname'] = $folder->name;
$prop['type'] = 'task';
$newfolder = kolab_storage::folder_update($prop);
@@ -254,12 +341,18 @@ class tasklist_kolab_driver extends tasklist_driver
* @param array Hash array with list properties
* id: List Identifier
* active: True if list is active, false if not
+ * permanent: True if list is to be subscribed permanently
* @return boolean True on success, Fales on failure
*/
public function subscribe_list($prop)
{
- if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
- return $folder->activate($prop['active']);
+ if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
+ $ret = false;
+ if (isset($prop['permanent']))
+ $ret |= $folder->subscribe(intval($prop['permanent']));
+ if (isset($prop['active']))
+ $ret |= $folder->activate(intval($prop['active']));
+ return $ret;
}
return false;
}
@@ -273,7 +366,7 @@ class tasklist_kolab_driver extends tasklist_driver
*/
public function remove_list($prop)
{
- if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
+ if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
if (kolab_storage::folder_delete($folder->name))
return true;
else
@@ -284,6 +377,63 @@ class tasklist_kolab_driver extends tasklist_driver
}
/**
+ * Search for shared or otherwise not listed tasklists the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of tasklists
+ */
+ public function search_lists($query, $source)
+ {
+ if (!kolab_storage::setup()) {
+ return array();
+ }
+
+ $this->search_more_results = false;
+ $this->lists = $this->folders = array();
+
+ $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
+
+ // find unsubscribed IMAP folders that have "event" type
+ if ($source == 'folders') {
+ foreach ((array)kolab_storage::search_folders('task', $query, array('other')) as $folder) {
+ $this->folders[$folder->id] = $folder;
+ $this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
+ }
+ }
+ // search other user's namespace via LDAP
+ else if ($source == 'users') {
+ $limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
+ foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
+ $folders = array();
+ // search for tasks folders shared by this user
+ foreach (kolab_storage::list_user_folders($user, 'task', false) as $foldername) {
+ $folders[] = new kolab_storage_folder($foldername, 'task');
+ }
+
+ if (count($folders)) {
+ $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
+ $this->folders[$userfolder->id] = $userfolder;
+ $this->lists[$userfolder->id] = $this->folder_props($userfolder, $delim, array());
+
+ foreach ($folders as $folder) {
+ $this->folders[$folder->id] = $folder;
+ $this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
+ $count++;
+ }
+ }
+
+ if ($count >= $limit) {
+ $this->search_more_results = true;
+ break;
+ }
+ }
+ }
+
+ return $this->get_lists();
+ }
+
+ /**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
@@ -303,7 +453,9 @@ class tasklist_kolab_driver extends tasklist_driver
$counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
foreach ($lists as $list_id) {
- $folder = $this->folders[$list_id];
+ if (!$folder = $this->get_folder($list_id)) {
+ continue;
+ }
foreach ($folder->select(array(array('tags','!~','x-complete'))) as $record) {
$rec = $this->_to_rcube_task($record);
@@ -367,7 +519,9 @@ class tasklist_kolab_driver extends tasklist_driver
}
foreach ($lists as $list_id) {
- $folder = $this->folders[$list_id];
+ if (!$folder = $this->get_folder($list_id)) {
+ continue;
+ }
foreach ($folder->select($query) as $record) {
$task = $this->_to_rcube_task($record);
$task['list'] = $list_id;
@@ -391,11 +545,11 @@ class tasklist_kolab_driver extends tasklist_driver
{
$id = is_array($prop) ? ($prop['uid'] ?: $prop['id']) : $prop;
$list_id = is_array($prop) ? $prop['list'] : null;
- $folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders;
+ $folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->folders;
// find task in the available folders
foreach ($folders as $list_id => $folder) {
- if (is_numeric($list_id))
+ if (is_numeric($list_id) || !$folder)
continue;
if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
$this->tasks[$id] = $this->_to_rcube_task($object);
@@ -424,7 +578,7 @@ class tasklist_kolab_driver extends tasklist_driver
$childs = array();
$list_id = $prop['list'];
$task_ids = array($prop['id']);
- $folder = $this->folders[$list_id];
+ $folder = $this->get_folder($list_id);
// query for childs (recursively)
while ($folder && !empty($task_ids)) {
@@ -484,7 +638,7 @@ class tasklist_kolab_driver extends tasklist_driver
if (!$list['showalarms'] || ($lists && !in_array($lid, $lists)))
continue;
- $folder = $this->folders[$lid];
+ $folder = $this->get_folder($lid);
foreach ($folder->select($query) as $record) {
if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-)
continue;
@@ -756,11 +910,11 @@ class tasklist_kolab_driver extends tasklist_driver
public function edit_task($task)
{
$list_id = $task['list'];
- if (!$list_id || !($folder = $this->folders[$list_id]))
+ if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
// moved from another folder
- if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
+ if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
if (!$fromfolder->move($task['id'], $folder->name))
return false;
@@ -809,11 +963,11 @@ class tasklist_kolab_driver extends tasklist_driver
public function move_task($task)
{
$list_id = $task['list'];
- if (!$list_id || !($folder = $this->folders[$list_id]))
+ if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
// execute move command
- if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
+ if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
return $fromfolder->move($task['id'], $folder->name);
}
@@ -831,7 +985,7 @@ class tasklist_kolab_driver extends tasklist_driver
public function delete_task($task, $force = true)
{
$list_id = $task['list'];
- if (!$list_id || !($folder = $this->folders[$list_id]))
+ if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
return $folder->delete($task['id']);
@@ -892,7 +1046,7 @@ class tasklist_kolab_driver extends tasklist_driver
*/
public function get_attachment_body($id, $task)
{
- if ($storage = $this->folders[$task['list']]) {
+ if ($storage = $this->get_folder($task['list'])) {
return $storage->get_attachment($task['id'], $id);
}
@@ -905,7 +1059,7 @@ class tasklist_kolab_driver extends tasklist_driver
public function tasklist_edit_form($action, $list, $fieldprop)
{
if ($list['id'] && ($list = $this->lists[$list['id']])) {
- $folder_name = $this->folders[$list['id']]->name; // UTF7
+ $folder_name = $this->get_folder($list['id'])->name; // UTF7
}
else {
$folder_name = '';
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index 6c31fa7..908c808 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -126,6 +126,15 @@ abstract class tasklist_driver
abstract function remove_list($prop);
/**
+ * Search for shared or otherwise not listed tasklists the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of tasklists
+ */
+ abstract function search_lists($query, $source);
+
+ /**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 18456fa..0195d64 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -5,6 +5,9 @@ $labels['navtitle'] = 'Tasks';
$labels['lists'] = 'Tasklists';
$labels['list'] = 'Tasklist';
$labels['tags'] = 'Tags';
+$labels['tasklistsubscribe'] = 'List permanently';
+$labels['listsearchresults'] = 'Available Tasklists';
+$labels['findlists'] = 'Find tasklists...';
$labels['newtask'] = 'New Task';
$labels['createnewtask'] = 'Create new Task (e.g. Saturday, Mow the lawn)';
diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png
index 5c6b9fd..1446573 100644
Binary files a/plugins/tasklist/skins/larry/sprites.png and b/plugins/tasklist/skins/larry/sprites.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index c940dff..f33f525 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -74,6 +74,32 @@ body.attachmentwin #topnav .topright {
bottom: 0px;
}
+#tasklistsbox .boxtitle a.iconbutton.search {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 16px;
+ cursor: pointer;
+ background-position: -2px -317px;
+}
+
+#tasklistsbox .listsearchbox {
+ display: none;
+}
+
+#tasklistsbox .listsearchbox.expanded {
+ display: block;
+}
+
+#tasklistsbox .scroller {
+ top: 34px;
+}
+
+#tasklistsbox .listsearchbox.expanded + .scroller {
+ top: 68px;
+}
+
+
#taskselector {
margin: -4px 40px 0 0;
padding: 0;
@@ -225,32 +251,48 @@ body.attachmentwin #topnav .topright {
display: none;
}
-#tasklists li {
+#tasklistsbox .treelist li {
+ margin: 0;
+ display: block;
+ position: relative;
+}
+
+#tasklistsbox .treelist li div.tasklist {
margin: 0;
height: 20px;
padding: 6px 8px 2px 6px;
- display: block;
position: relative;
white-space: nowrap;
}
-#tasklists li.virtual {
- height: 12px;
+#tasklistsbox .treelist li.virtual > div.tasklist {
+ height: 14px;
+}
+
+#tasklistsbox .treelist ul li > div.tasklist {
+ margin-left: 16px;
+}
+
+#tasklistsbox .treelist ul ul li > div.tasklist {
+ margin-left: 32px;
+}
+
+#tasklistsbox .treelist ul ul ul li > div.tasklist {
+ margin-left: 48px;
}
-#tasklists li label {
+#tasklistsbox .treelist li label {
display: block;
}
-#tasklists li span.listname {
+#tasklistsbox .treelist li span.listname {
display: block;
position: absolute;
top: 7px;
- left: 26px;
- right: 26px;
+ left: 38px;
+ right: 40px;
cursor: default;
- padding-bottom: 2px;
- padding-right: 30px;
+ padding: 0px 30px 2px 2px;
color: #004458;
overflow: hidden;
text-overflow: ellipsis;
@@ -258,8 +300,11 @@ body.attachmentwin #topnav .topright {
background: url(sprites.png) right 20px no-repeat;
}
-#tasklists li span.handle {
+#tasklistsbox .treelist li span.quickview {
display: inline-block;
+ position: absolute;
+ top: 6px;
+ right: 20px;
width: 16px;
height: 16px;
margin-right: 4px;
@@ -267,52 +312,76 @@ body.attachmentwin #topnav .topright {
cursor: pointer;
}
-#tasklists li:hover span.handle {
- background-position: -20px -101px;
+#tasklistsbox .treelist li a.subscribed {
+ display: inline-block;
+ position: absolute;
+ top: 6px;
+ right: 5px;
+ height: 16px;
+ width: 16px;
+ padding: 0;
+ background: url(sprites.png) -100px 0 no-repeat;
+ overflow: hidden;
+ text-indent: -5000px;
+ cursor: pointer;
}
-#tasklists li.focusview span.handle {
- background-position: -2px -101px;
+#tasklistsbox .treelist div:hover > a.subscribed {
+ background-position: -2px -215px;
}
-#tasklists li.selected span.listname {
- font-weight: bold;
+#tasklistsbox .treelist div.subscribed a.subscribed {
+ background-position: -20px -215px;
}
-#tasklists li.readonly span.listname {
- background-position: right -142px;
+#tasklistsbox .treelist li div:hover > span.quickview {
+ background-position: -20px -101px;
}
-#tasklists li.other span.listname {
- background-position: right -160px;
+#tasklistsbox .treelist li div.focusview > span.quickview {
+ background-position: -2px -101px;
}
-#tasklists li.other.readonly span.listname {
- background-position: right -178px;
+#tasklistsbox .searchresults .treelist li span.quickview {
+ display: none;
+}
+
+#tasklistsbox .treelist li.selected > div > span.listname {
+ font-weight: bold;
}
-#tasklists li.shared span.listname {
- background-position: right -196px;
+#tasklistsbox .treelist .readonly > span.listname {
+ background-position: right -142px;
}
-#tasklists li.shared.readonly span.listname {
- background-position: right -214px;
+#tasklistsbox .treelist .user > span.listname {
+ background-position: right -160px;
}
-#tasklists li.virtual span.listname {
+#tasklistsbox .treelist .virtual > span.listname {
color: #aaa;
- top: 2px;
+ top: 4px;
+ left: 20px;
+ right: 5px;
}
-#tasklists li.virtual span.handle {
- background: none;
- cursor: default;
+#tasklistsbox .treelist.flat li span.calname {
+ left: 24px;
+ right: 22px;
}
-#tasklists li input {
+#tasklistsbox .treelist li input {
position: absolute;
top: 5px;
- right: 5px;
+ left: 18px;
+}
+
+#tasklistsbox .treelist li .treetoggle {
+ top: 8px;
+}
+
+#tasklistsbox .treelist li.virtual > .treetoggle {
+ top: 6px;
}
#mainview-right {
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index fe3f88b..0686646 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -24,9 +24,18 @@
</div>
<div id="tasklistsbox" class="uibox listbox">
- <h2 class="boxtitle"><roundcube:label name="tasklist.lists" /></h2>
+ <h2 class="boxtitle"><roundcube:label name="tasklist.lists" />
+ <a class="iconbutton search" title="<roundcube:label name='tasklist.findlists' />"></a>
+ </h2>
+ <div class="listsearchbox">
+ <div class="searchbox">
+ <input type="text" name="q" id="tasklistsearch" placeholder="<roundcube:label name='tasklist.findlists' />" />
+ <a class="iconbutton searchicon"></a>
+ <roundcube:button command="reset-listsearch" id="tasklistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
+ </div>
+ </div>
<div class="scroller withfooter">
- <roundcube:object name="plugin.tasklists" id="tasklists" class="listing" />
+ <roundcube:object name="plugin.tasklists" id="tasklists" class="treelist listing" />
</div>
<div class="boxfooter">
<roundcube:button command="list-create" type="link" title="tasklist.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="tasklistoptionslink" id="tasklistoptionsmenulink" type="link" title="tasklist.listactions" class="listbutton groupactions" onclick="UI.show_popup('tasklistoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="⚙" />
@@ -171,6 +180,30 @@ $(document).ready(function(e){
orientation:'v', relative:true, start:240, min:180, size:12 }).init();
new rcube_splitter({ id:'taskviewsplitterv', p1:'#tagsbox', p2:'#tasklistsbox',
orientation:'h', relative:true, start:242, min:120, size:12, offset:4 }).init();
+
+ // animation to unfold list search box
+ $('#tasklistsbox .boxtitle a.search').click(function(e){
+ var box = $('#tasklistsbox .listsearchbox'),
+ dir = box.is(':visible') ? -1 : 1;
+
+ box.slideToggle({
+ duration: 160,
+ progress: function(animation, progress) {
+ if (dir < 0) progress = 1 - progress;
+ $('#tasklistsbox .scroller').css('top', (34 + 34 * progress) + 'px');
+ },
+ complete: function() {
+ box.toggleClass('expanded');
+ if (box.is(':visible')) {
+ box.find('input[type=text]').focus();
+ }
+ else {
+ $('#tasklistsearch-reset').click();
+ }
+ // TODO: save state in localStorage
+ }
+ });
+ });
});
</script>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index fcf4306..774e9b4 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -79,6 +79,7 @@ function rcube_tasklist_ui(settings)
var scroll_speed = 20;
var scroll_sensitivity = 40;
var scroll_timer;
+ var tasklists_widget;
var me = this;
// general datepicker settings
@@ -127,18 +128,80 @@ function rcube_tasklist_ui(settings)
{
// initialize task list selectors
for (var id in me.tasklists) {
- if ((li = rcmail.get_folder_li(id, 'rcmlitasklist'))) {
- init_tasklist_li(li, id);
- }
-
if (me.tasklists[id].editable && (!me.selected_list || (me.tasklists[id].active && !me.tasklists[me.selected_list].active))) {
me.selected_list = id;
+ break;
}
}
+ // initialize treelist widget that controls the tasklists list
+ var widget_class = window.kolab_folderlist || rcube_treelist_widget;
+ tasklists_widget = new widget_class(rcmail.gui_objects.tasklistslist, {
+ id_prefix: 'rcmlitasklist',
+ selectable: true,
+ save_state: true,
+ searchbox: '#tasklistsearch',
+ search_action: 'tasks/tasklist',
+ search_sources: [ 'folders', 'users' ],
+ search_title: rcmail.gettext('listsearchresults','tasklist')
+ });
+ tasklists_widget.addEventListener('select', function(node) {
+ var id = $(this).data('id');
+ rcmail.enable_command('list-edit', 'list-remove', 'list-import', me.tasklists[node.id].editable);
+ me.selected_list = node.id;
+ });
+ tasklists_widget.addEventListener('subscribe', function(p) {
+ var list;
+ if ((list = me.tasklists[p.id])) {
+ list.subscribed = p.subscribed || false;
+ rcmail.http_post('tasklist', { action:'subscribe', l:{ id:p.id, active:list.active?1:0, permanent:list.subscribed?1:0 } });
+ }
+ });
+ tasklists_widget.addEventListener('insert-item', function(p) {
+ var list = p.data;
+ if (list && list.id && !list.virtual) {
+ me.tasklists[list.id] = list;
+ var prop = { id:p.id, active:list.active?1:0 };
+ if (list.subscribed) prop.permanent = 1;
+ rcmail.http_post('tasklist', { action:'subscribe', l:prop });
+ list_tasks();
+ }
+ });
+
+ // init (delegate) event handler on tasklist checkboxes
+ tasklists_widget.container.on('click', 'input[type=checkbox]', function(e){
+ var list, id = this.value;
+ if ((list = me.tasklists[id])) {
+ list.active = this.checked;
+ fetch_counts();
+ if (!this.checked) remove_tasks(id);
+ else list_tasks(null);
+ rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:list.active?1:0 } });
+
+ // disable focusview
+ if (!this.checked && focusview == id) {
+ set_focusview(null);
+ }
+ }
+ e.stopPropagation();
+ });
+
+ // handler for clicks on quickview buttons
+ tasklists_widget.container.on('click', '.quickview', function(e){
+ var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, '');
+ set_focusview(focusview == id ? null : id)
+ e.stopPropagation();
+ });
+
+ // register dbl-click handler to open calendar edit dialog
+ tasklists_widget.container.on('dblclick', ':not(.virtual) > .tasklist', function(e){
+ var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, '');
+ list_edit_dialog(id);
+ });
+
if (me.selected_list) {
rcmail.enable_command('addtask', true);
- $(rcmail.get_folder_li(me.selected_list, 'rcmlitasklist')).click();
+ tasklists_widget.select(me.selected_list);
}
// register server callbacks
@@ -1972,7 +2035,7 @@ function rcube_tasklist_ui(settings)
me.selected_list = id;
// click on handle icon toggles focusview
- if (e.target.className == 'handle') {
+ if (e.target.className == 'quickview') {
set_focusview(focusview == id ? null : id)
}
// disable focusview when selecting another list
@@ -1994,13 +2057,15 @@ function rcube_tasklist_ui(settings)
function set_focusview(id)
{
if (focusview && focusview != id)
- $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).removeClass('focusview');
+ $(tasklists_widget.get_item(focusview)).find('.tasklist').first().removeClass('focusview');
focusview = id;
+ var li = $(tasklists_widget.get_item(id)).find('.tasklist').first();
+
// activate list if necessary
if (focusview && !me.tasklists[id].active) {
- $('input', rcmail.get_folder_li(id, 'rcmlitasklist')).get(0).checked = true;
+ li.find('input[type=checkbox]').get(0).checked = true;
me.tasklists[id].active = true;
fetch_counts();
}
@@ -2009,7 +2074,7 @@ function rcube_tasklist_ui(settings)
list_tasks(null);
if (focusview) {
- $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).addClass('focusview');
+ li.addClass('focusview');
}
}
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 65376d7..1a70699 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -52,6 +52,7 @@ class tasklist extends rcube_plugin
public $driver;
public $timezone;
public $ui;
+ public $home; // declare public to be used in other classes
private $collapsed_tasks = array();
@@ -134,8 +135,7 @@ class tasklist extends rcube_plugin
}
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
- require_once($this->home . '/tasklist_ui.php');
- $this->ui = new tasklist_ui($this);
+ $this->load_ui();
$this->ui->init();
}
@@ -144,6 +144,16 @@ class tasklist extends rcube_plugin
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
}
+ /**
+ *
+ */
+ private function load_ui()
+ {
+ if (!$this->ui) {
+ require_once($this->home . '/tasklist_ui.php');
+ $this->ui = new tasklist_ui($this);
+ }
+ }
/**
* Helper method to load the backend driver according to local config
@@ -619,6 +629,30 @@ class tasklist extends rcube_plugin
if (($success = $this->driver->remove_list($list)))
$this->rc->output->command('plugin.destroy_tasklist', $list);
break;
+
+ case 'search':
+ $this->load_ui();
+ $results = array();
+ foreach ((array)$this->driver->search_lists(get_input_value('q', RCUBE_INPUT_GPC), get_input_value('source', RCUBE_INPUT_GPC)) as $id => $prop) {
+ $editname = $prop['editname'];
+ unset($prop['editname']); // force full name to be displayed
+ $prop['active'] = false;
+
+ // let the UI generate HTML and CSS representation for this calendar
+ $html = $this->ui->tasklist_list_item($id, $prop, $jsenv);
+ $prop += (array)$jsenv[$id];
+ $prop['editname'] = $editname;
+ $prop['html'] = $html;
+
+ $results[] = $prop;
+ }
+ // report more results available
+ if ($this->driver->search_more_results) {
+ $this->rc->output->show_message('autocompletemore', 'info');
+ }
+
+ $this->rc->output->command('multi_thread_http_response', $results, get_input_value('_reqid', RCUBE_INPUT_GPC));
+ return;
}
if ($success)
@@ -875,6 +909,13 @@ class tasklist extends rcube_plugin
{
$this->ui->init();
$this->ui->init_templates();
+
+ // set autocompletion env
+ $this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
+ $this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
+ $this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
+ $this->rc->output->add_label('autocompletechars', 'autocompletemore');
+
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');
}
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 6988a61..6f92d26 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -81,6 +81,12 @@ class tasklist_ui
$this->plugin->include_script('jquery.tagedit.js');
$this->plugin->include_script('tasklist.js');
+ $this->rc->output->include_script('treelist.js');
+
+ // include kolab folderlist widget if available
+ if (is_readable($this->plugin->api->dir . 'libkolab/js/folderlist.js')) {
+ $this->plugin->api->include_script('libkolab/js/folderlist.js');
+ }
$this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tagedit.css');
}
@@ -88,45 +94,110 @@ class tasklist_ui
/**
*
*/
- function tasklists($attrib = array())
+ public function tasklists($attrib = array())
+ {
+ $tree = true;
+ $jsenv = array();
+ $lists = $this->plugin->driver->get_lists($tree);
+
+ // walk folder tree
+ if (is_object($tree)) {
+ $html = $this->list_tree_html($tree, $lists, $jsenv, $attrib);
+ }
+ else {
+ // fall-back to flat folder listing
+ $attrib['class'] .= ' flat';
+
+ $html = '';
+ foreach ((array)$lists as $id => $prop) {
+ if ($attrib['activeonly'] && !$prop['active'])
+ continue;
+
+ $html .= html::tag('li', array(
+ 'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
+ 'class' => $prop['group'],
+ ),
+ $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly'])
+ );
+ }
+ }
+
+ $this->rc->output->set_env('tasklists', $jsenv);
+ $this->rc->output->add_gui_object('tasklistslist', $attrib['id']);
+
+ return html::tag('ul', $attrib, $html, html::$common_attrib);
+ }
+
+ /**
+ * Return html for a structured list <ul> for the folder tree
+ */
+ public function list_tree_html($node, $data, &$jsenv, $attrib)
{
- $lists = $this->plugin->driver->get_lists();
+ $out = '';
+ foreach ($node->children as $folder) {
+ $id = $folder->id;
+ $prop = $data[$id];
+ $is_collapsed = false; // TODO: determine this somehow?
+
+ $content = $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly']);
+
+ if (!empty($folder->children)) {
+ $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
+ $this->list_tree_html($folder, $data, $jsenv, $attrib));
+ }
+
+ if (strlen($content)) {
+ $out .= html::tag('li', array(
+ 'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
+ 'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
+ ),
+ $content);
+ }
+ }
- $li = '';
- foreach ((array)$lists as $id => $prop) {
- if ($attrib['activeonly'] && !$prop['active'])
- continue;
+ return $out;
+ }
+ /**
+ * Helper method to build a tasklist item (HTML content and js data)
+ */
+ public function tasklist_list_item($id, $prop, &$jsenv, $activeonly = false)
+ {
+ // enrich list properties with settings from the driver
+ if (!$prop['virtual']) {
unset($prop['user_id']);
$prop['alarms'] = $this->plugin->driver->alarms;
$prop['undelete'] = $this->plugin->driver->undelete;
$prop['sortable'] = $this->plugin->driver->sortable;
$prop['attachments'] = $this->plugin->driver->attachments;
-
- if (!$prop['virtual'])
- $jsenv[$id] = $prop;
-
- $html_id = html_identifier($id);
- $class = 'tasks-' . asciiwords($id, true);
- $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
-
- if ($prop['virtual'])
- $class .= ' virtual';
- else if (!$prop['editable'])
- $class .= ' readonly';
- if ($prop['class_name'])
- $class .= ' '.$prop['class_name'];
-
- $li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class),
- ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active']))) .
- html::span(array('class' => 'handle', 'title' => $this->plugin->gettext('focusview')), ' ') .
- html::span(array('class' => 'listname', 'title' => $title), $prop['listname']));
+ $jsenv[$id] = $prop;
}
- $this->rc->output->set_env('tasklists', $jsenv);
- $this->rc->output->add_gui_object('folderlist', $attrib['id']);
+ $classes = array('tasklist');
+ $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+ html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
+
+ if ($prop['virtual'])
+ $classes[] = 'virtual';
+ else if (!$prop['editable'])
+ $classes[] = 'readonly';
+ if ($prop['subscribed'])
+ $classes[] = 'subscribed';
+ if ($prop['class'])
+ $classes[] = $prop['class'];
+
+ if (!$activeonly || $prop['active']) {
+ return html::div(join(' ', $classes),
+ html::span(array('class' => 'listname', 'title' => $title), $prop['listname']) .
+ ($prop['virtual'] ? '' :
+ html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
+ html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), ' ') .
+ (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe')), ' ') : '')
+ )
+ );
+ }
- return html::tag('ul', $attrib, $li, html::$common_attrib);
+ return '';
}
commit 184730b3477ea1c7410b82913a0282baeb6a5bb2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed May 21 12:44:26 2014 +0200
Remove invisible text because that's considered for searching
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index f973e88..70e0c37 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -307,7 +307,7 @@ class calendar_ui
html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
($prop['virtual'] ? '' :
html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
- (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), $this->cal->gettext('subscribed')) : '') .
+ (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), ' ') : '') .
html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')
)
);
commit 709bd160167ad33b8c7a86d12213bcd06d7d38bc
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed May 21 10:50:36 2014 +0200
Minor bugfixes and visual enhancements for new folder navigation
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index c845c15..3e5d28a 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -52,7 +52,7 @@ class kolab_calendar extends kolab_storage_folder_api
$imap = $calendar->rc->get_storage();
$imap_folder = kolab_storage::id_decode($id);
$info = $imap->folder_info($imap_folder, true);
- if (empty($info) || $info['noselect'] || kolab_storage::folder_type($imap_folder) != 'event') {
+ if (empty($info) || $info['noselect'] || strpos(kolab_storage::folder_type($imap_folder), 'event') !== 0) {
return new kolab_user_calendar($imap_folder, $calendar);
}
else {
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 217fe42..a95e1c2 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -117,8 +117,9 @@ class kolab_driver extends calendar_driver
}
}
+ $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$folders = $this->filter_calendars(false, $active, $personal);
- $calendars = $names = array();
+ $calendars = array();
// include virtual folders for a full folder tree
if (!is_null($tree))
@@ -127,14 +128,19 @@ class kolab_driver extends calendar_driver
foreach ($folders as $id => $cal) {
$fullname = $cal->get_name();
$listname = $cal->get_foldername();
- $imap_path = explode('/', $cal->name);
+ $imap_path = explode($delim, $cal->name);
// find parent
do {
array_pop($imap_path);
- $parent_id = kolab_storage::folder_id(join('/', $imap_path));
+ $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
+ }
+ while (count($imap_path) > 1 && !$this->calendars[$parent_id]);
+
+ // restore "real" parent ID
+ if ($parent_id && !$this->calendars[$parent_id]) {
+ $parent_id = kolab_storage::folder_id($cal->get_parent());
}
- while (count($imap_path) > 0 && !$this->calendars[$parent_id]);
// turn a kolab_storage_folder object into a kolab_calendar
if ($cal instanceof kolab_storage_folder) {
@@ -360,9 +366,9 @@ class kolab_driver extends calendar_driver
if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
$ret = false;
if (isset($prop['permanent']))
- $ret |= $cal->storage->subscribe($prop['permanent']);
+ $ret |= $cal->storage->subscribe(intval($prop['permanent']));
if (isset($prop['active']))
- $ret |= $cal->storage->activate($prop['active']);
+ $ret |= $cal->storage->activate(intval($prop['active']));
return $ret;
}
else {
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 3329bf4..f973e88 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -273,7 +273,7 @@ class calendar_ui
/**
* Helper method to build a calendar list item (HTML content and js data)
*/
- public function calendar_list_item($id, $prop, &$jsenv, $activeonly)
+ public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
{
// enrich calendar properties with settings from the driver
if (!$prop['virtual']) {
diff --git a/plugins/calendar/skins/larry/images/calendars.png b/plugins/calendar/skins/larry/images/calendars.png
index 1f97abc..117d329 100644
Binary files a/plugins/calendar/skins/larry/images/calendars.png and b/plugins/calendar/skins/larry/images/calendars.png differ
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index ce07112..2e45fcf 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -256,6 +256,9 @@ $(document).ready(function(e){
if (box.is(':visible')) {
box.find('input[type=text]').focus();
}
+ else {
+ $('#calendarlistsearch-reset').click();
+ }
// TODO: save state in localStorage
}
});
commit 36248fb46897c56c0a17bc010fc453c37d00d72c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue May 20 20:26:47 2014 +0200
Code cleanup and small fixes
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 62b19f7..3329bf4 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -230,7 +230,7 @@ class calendar_ui
continue;
$html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']),
- $content = $this->calendar_list_item($id, $prop, $jsenv)
+ $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly'])
);
}
@@ -249,8 +249,9 @@ class calendar_ui
foreach ($node->children as $folder) {
$id = $folder->id;
$prop = $data[$id];
+ $is_collapsed = false; // TODO: determine this somehow?
- $content = $this->calendar_list_item($id, $prop, $jsenv);
+ $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly']);
if (!empty($folder->children)) {
$content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
@@ -272,7 +273,7 @@ class calendar_ui
/**
* Helper method to build a calendar list item (HTML content and js data)
*/
- public function calendar_list_item($id, $prop, &$jsenv)
+ public function calendar_list_item($id, $prop, &$jsenv, $activeonly)
{
// enrich calendar properties with settings from the driver
if (!$prop['virtual']) {
@@ -290,19 +291,18 @@ class calendar_ui
$classes = array('calendar', 'cal-' . asciiwords($id, true));
$title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
- $is_collapsed = false; // TODO: determine this somehow?
if ($prop['virtual'])
$classes[] = 'virtual';
else if ($prop['readonly'])
$classes[] = 'readonly';
if ($prop['subscribed'])
- $classes[] = ' subscribed';
+ $classes[] = 'subscribed';
if ($prop['class'])
$classes[] = $prop['class'];
$content = '';
- if (!$attrib['activeonly'] || $prop['active']) {
+ if (!$activeonly || $prop['active']) {
$content = html::div(join(' ', $classes),
html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
($prop['virtual'] ? '' :
commit 96359552f76d84da3467f5c6ea6652b19bc7720a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue May 20 13:55:53 2014 +0200
Add extended splitter to calendar view that allows resizing and collapsing the sidebar
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 021f4fb..adca758 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -109,21 +109,21 @@ body.attachmentwin #topnav .topright {
top: 4px;
}
-#calendarsidebartoggle {
+#calsidebarsplitter {
position: absolute;
left: 264px;
- width: 8px;
- top: 40px;
+ width: 6px;
+ top: 40px !important;
bottom: 0;
- background: url(images/toggle.gif) 0 48% no-repeat transparent;
- cursor: pointer;
+ background: url(images/toggle.gif) -1px 48% no-repeat transparent;
}
div.sidebarclosed {
background-position: -8px 48% !important;
+ cursor: pointer;
}
-#calendarsidebartoggle:hover {
+#calsidebarsplitter:hover {
background-color: #ddd;
}
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 2a76798..ce07112 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -40,7 +40,6 @@
</div>
</div>
</div>
- <div id="calendarsidebartoggle"></div>
<div id="quicksearchbar">
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
@@ -235,25 +234,11 @@ var UI = new rcube_mail_ui();
$(document).ready(function(e){
UI.init();
- // initialize sidebar toggle
- $('#calendarsidebartoggle').click(function() {
- var width = $(this).data('sidebarwidth');
- var offset = $(this).data('offset');
- var $sidebar = $('#calendarsidebar'), time = 250;
-
- if ($sidebar.is(':visible')) {
- $sidebar.animate({ left:'-'+(width+10)+'px' }, time, function(){ $('#calendarsidebar').hide(); });
- $(this).animate({ left:'8px'}, time, function(){ $('#calendarsidebartoggle').addClass('sidebarclosed') });
- $('#calendar').animate({ left:'20px'}, time, function(){ $(this).fullCalendar('render'); });
- }
- else {
- $sidebar.show().animate({ left:'10px' }, time);
- $(this).animate({ left:offset+'px'}, time, function(){ $('#calendarsidebartoggle').removeClass('sidebarclosed'); });
- $('#calendar').animate({ left:(width+16)+'px'}, time, function(){ $(this).fullCalendar('render'); });
- }
- })
- .data('offset', $('#calendarsidebartoggle').position().left)
- .data('sidebarwidth', $('#calendarsidebar').width() + $('#calendarsidebar').position().left);
+ new calendarview_splitter({ id:'calsidebarsplitter', p1:'#calendarsidebar', p2:'#calendar',
+ orientation:'v', relative:true, start:270, min:240, size:12, offset:0 });
+
+ new rcube_splitter({ id:'calresourceviewsplitter', p1:'#resource-dialog-left', p2:'#resource-dialog-right',
+ orientation:'v', relative:true, start:380, min:220, size:10, offset:-3 }).init();
// animation to unfold list search box
$('#calendars .boxtitle a.search').click(function(e){
@@ -276,10 +261,123 @@ $(document).ready(function(e){
});
});
- new rcube_splitter({ id:'calresourceviewsplitter', p1:'#resource-dialog-left', p2:'#resource-dialog-right',
- orientation:'v', relative:true, start:380, min:220, size:10, offset:-3 }).init();
});
+
+/**
+ * Extended rcube_splitter class that entirely collapses the calendar sidebar
+ */
+function calendarview_splitter(p)
+{
+ this.collapsed = false;
+ this.dragging = false;
+ this.threshold = 80;
+ this.lastpos = 0;
+ this._lastpos = 0;
+ this._min = p.min;
+
+ var me = this;
+ p.callback = function(e){
+ if (me.lastpos != me._lastpos) {
+ me.dragging = true;
+ setTimeout(function(){ me.dragging = false; }, 50);
+ me._lastpos = me.lastpos;
+ }
+ };
+
+ // extend base class
+ p.min = 20;
+ rcube_splitter.call(this, p);
+
+ // @override
+ this.resize = function()
+ {
+ if (this.pos < this.threshold) {
+ if (!this.collapsed)
+ this.collapse();
+ }
+ else if (this.pos < this._min && this.pos > this._min / 2) {
+ if (this.collapsed)
+ this.expand();
+ }
+ else if (this.pos >= this._min) {
+ this.p1.css('width', Math.floor(this.pos - this.p1pos.left - this.halfsize) + 'px');
+ this.p2.css('left', Math.ceil(this.pos + this.halfsize) + 'px');
+ this.handle.css('left', Math.round(this.pos - this.halfsize + this.offset + 3)+'px');
+ if (bw.ie) {
+ var new_width = parseInt(this.parent.outerWidth(), 10) - parseInt(this.p2.css('left'), 10) ;
+ this.p2.css('width', (new_width > 0 ? new_width : 0) + 'px');
+ }
+
+ this.p2.resize();
+ this.p1.resize();
+ this.lastpos = this.pos;
+
+ // also resize iframe covers
+ if (this.drag_active) {
+ $('iframe').each(function(i, elem) {
+ var pos = $(this).offset();
+ $('#iframe-splitter-fix-'+i).css({ top: pos.top+'px', left: pos.left+'px', width:elem.offsetWidth+'px', height: elem.offsetHeight+'px' });
+ });
+ }
+
+ if (typeof this.render == 'function')
+ this.render(this);
+ }
+ }
+
+ this.collapse = function(animated)
+ {
+ var me = this, time = 250;
+ if (animated) {
+ this.p1.animate({ left:'0px' }, time, function(){ $(this).hide(); });
+ this.p2.animate({ left:this.p.size + 'px' }, time, function(){ $(this).resize(); });
+ this.handle.animate({ left:'3px'}, time, function(){ $(this).addClass('sidebarclosed') });
+ }
+ else {
+ this.p1.css('left', 0).hide();
+ this.p2.css('left', this.p.size + 'px');
+ this.handle.css('left', '3px').addClass('sidebarclosed');
+ this.p2.resize();
+ }
+
+ // stop dragging
+ if (this.drag_active) {
+ this.drag_active = false;
+ $(document).unbind('.'+this.id);
+ $('div.iframe-splitter-fix').remove();
+ }
+
+ this.pos = 10;
+ this.collapsed = true;
+ this.set_cookie();
+ }
+
+ this.expand = function()
+ {
+ var me = this, time = 250;
+ this.handle.removeClass('sidebarclosed');
+ this.pos = this.lastpos || this._min;
+ this.p1pos.left = 10;
+ this.p1.show().animate({ left:'10px', width:(this.pos - this.p1pos.left - this.halfsize) + 'px' }, time);
+ this.p2.animate({ left:(this.pos + this.halfsize) + 'px' }, time, function(){ me.resize(); });
+ this.handle.animate({ left:(this.pos - this.halfsize + this.offset + 3) + 'px' }, time);
+
+ this.collapsed = false;
+ this.set_cookie();
+ }
+
+ this.init();
+
+ var me = this;
+ this.handle.bind('click', function(e){
+ if (!me.collapsed && !me.dragging)
+ me.collapse(true);
+ else if (!me.dragging)
+ me.expand();
+ });
+}
+
</script>
</body>
commit af416196d989560b7aa5086276e7fa034225a627
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue May 20 12:04:22 2014 +0200
Hide calendars search box and toggle on click of the search icon
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 5ef63f7..021f4fb 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -164,7 +164,32 @@ pre {
right: 0;
}
+#calendars .boxtitle {
+ position: relative;
+}
+
+#calendars .boxtitle a.iconbutton.search {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 16px;
+ cursor: pointer;
+ background-position: -2px -317px;
+}
+
+#calendars .listsearchbox {
+ display: none;
+}
+
+#calendars .listsearchbox.expanded {
+ display: block;
+}
+
#calendars .scroller {
+ top: 34px;
+}
+
+#calendars .listsearchbox.expanded + .scroller {
top: 68px;
}
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 7f9f0af..2a76798 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -22,7 +22,9 @@
<div id="datepicker" class="uibox"></div>
<div id="calendars" class="uibox listbox" style="visibility:hidden">
- <h2 class="boxtitle"><roundcube:label name="calendar.calendars" /></h2>
+ <h2 class="boxtitle"><roundcube:label name="calendar.calendars" />
+ <a class="iconbutton search" title="<roundcube:label name='calendar.findcalendars' />"></a>
+ </h2>
<div class="listsearchbox">
<div class="searchbox">
<input type="text" name="q" id="calendarlistsearch" placeholder="<roundcube:label name='calendar.findcalendars' />" />
@@ -253,6 +255,27 @@ $(document).ready(function(e){
.data('offset', $('#calendarsidebartoggle').position().left)
.data('sidebarwidth', $('#calendarsidebar').width() + $('#calendarsidebar').position().left);
+ // animation to unfold list search box
+ $('#calendars .boxtitle a.search').click(function(e){
+ var box = $('#calendars .listsearchbox'),
+ dir = box.is(':visible') ? -1 : 1;
+
+ box.slideToggle({
+ duration: 160,
+ progress: function(animation, progress) {
+ if (dir < 0) progress = 1 - progress;
+ $('#calendars .scroller').css('top', (34 + 34 * progress) + 'px');
+ },
+ complete: function() {
+ box.toggleClass('expanded');
+ if (box.is(':visible')) {
+ box.find('input[type=text]').focus();
+ }
+ // TODO: save state in localStorage
+ }
+ });
+ });
+
new rcube_splitter({ id:'calresourceviewsplitter', p1:'#resource-dialog-left', p2:'#resource-dialog-right',
orientation:'v', relative:true, start:380, min:220, size:10, offset:-3 }).init();
});
commit d569bb86440092d817799a09bdcd0de1dd657bbb
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue May 20 12:03:24 2014 +0200
Adapt classic skin to new calendars listing
diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 40350fa..0f3ba5f 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -105,6 +105,14 @@ pre {
overflow: hidden;
}
+#calendars .boxlistcontent {
+ top: 43px;
+}
+
+#calendars .listsearchbox {
+ padding: 2px 4px;
+}
+
#calendarslist {
list-style: none;
margin: 0;
@@ -122,54 +130,143 @@ pre {
cursor: default;
}
-#calendarslist li label {
- display: block;
+#calendars .treelist li {
+ margin: 0;
+ padding: 0;
+ position: relative;
}
-#calendarslist li span.handle {
+#calendars .treelist ul li:last-child {
+ border-bottom: 0;
+}
+
+#calendars .treelist li div.folder,
+#calendars .treelist li div.calendar {
+ position: relative;
+ height: 22px;
+}
+
+#calendars .treelist li span.calname {
+ display: block;
+ padding: 0px 30px 2px 2px;
+ position: absolute;
+ top: 4px;
+ left: 38px;
+ right: 40px;
cursor: default;
- background: url(images/calendars.png) 0 -2px no-repeat;
+ background: url(images/calendars.png) right 20px no-repeat;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#calendars .treelist li div.virtual > span.calname {
+ color: #aaa;
+ left: 20px;
+}
+
+#calendars .treelist.flat li span.calname {
+ left: 24px;
+ right: 22px;
+}
+
+#calendars .treelist li span.handle {
display: inline-block;
- width: 20px;
+ position: absolute;
+ top: 5px;
+ right: 6px;
+ padding: 0;
+ width: 12px;
+ height: 12px;
+ border-radius: 3px;
+ font-size: 0.8em;
}
-#calendarslist li input {
- margin-right: 5px;
+#calendars .treelist li a.subscribed {
+ display: inline-block;
+ position: absolute;
+ top: 2px;
+ right: 22px;
+ height: 16px;
+ width: 16px;
+ padding: 0;
+ background: url(images/calendars.png) -100px 0 no-repeat;
+ overflow: hidden;
+ text-indent: -5000px;
+ cursor: pointer;
}
-#calendarslist li.selected {
- background-color: #ccc;
- border-bottom: 1px solid #bbb;
+#calendars .treelist div:hover > a.subscribed {
+ background-position: 0 -126px;
}
-#calendarslist li.selected span {
- font-weight: bold;
+#calendars .treelist div.subscribed a.subscribed {
+ background-position: 0 -144px;
+}
+
+#calendars .treelist li input {
+ position: absolute;
+ top: 1px;
+ left: 18px;
+}
+
+#calendars .treelist li div.treetoggle {
+ top: -1px;
+ left: 1px !important;
}
-#calendarslist li.readonly span.handle {
- background-position: 0 -20px;
+#calendars .treelist ul li div.treetoggle {
+ left: 17px !important;
}
-#calendarslist li.other span.handle {
- background-position: 0 -38px;
+#calendars .treelist ul ul li div.treetoggle {
+ left: 33px !important;
}
-#calendarslist li.other.readonly span.handle {
- background-position: 0 -56px;
+#calendars .treelist.flat li input {
+ left: 4px;
}
-#calendarslist li.shared span.handle {
- background-position: 0 -74px;
+#calendars .treelist ul li div.folder,
+#calendars .treelist ul li div.calendar {
+ margin-left: 16px;
}
-#calendarslist li.shared.readonly span.handle {
- background-position: 0 -92px;
+#calendars .treelist ul ul li div.folder,
+#calendars .treelist ul ul li div.calendar {
+ margin-left: 32px;
+}
+
+#calendars .treelist ul ul ul li div.folder,
+#calendars .treelist ul ul ul li div.calendar {
+ margin-left: 48px;
+}
+
+#calendars .treelist li.selected {
+ background-color: #ccc;
+}
+
+#calendars .treelist li.selected > span.calname {
+ font-weight: bold;
+}
+
+#calendars .treelist div.readonly span.calname {
+ background-position: right -20px;
+}
+
+#calendars .treelist li.user > div > span.calname {
+ background-position: right -38px;
}
#calendarslist li.virtual span.calname {
color: #666;
}
+#calendars .searchresults .boxtitle {
+ border-top: 1px solid #aaa;
+ margin-bottom: 0;
+}
+
#calfeedurl,
#caldavurl {
width: 98%;
diff --git a/plugins/calendar/skins/classic/images/calendars.gif b/plugins/calendar/skins/classic/images/calendars.gif
index cf12ebd..c560e74 100644
Binary files a/plugins/calendar/skins/classic/images/calendars.gif and b/plugins/calendar/skins/classic/images/calendars.gif differ
diff --git a/plugins/calendar/skins/classic/images/calendars.png b/plugins/calendar/skins/classic/images/calendars.png
index feb3945..e411c41 100644
Binary files a/plugins/calendar/skins/classic/images/calendars.png and b/plugins/calendar/skins/classic/images/calendars.png differ
diff --git a/plugins/calendar/skins/classic/templates/calendar.html b/plugins/calendar/skins/classic/templates/calendar.html
index fa93afc..219fd83 100644
--- a/plugins/calendar/skins/classic/templates/calendar.html
+++ b/plugins/calendar/skins/classic/templates/calendar.html
@@ -24,8 +24,15 @@
<div id="datepicker"></div>
<div id="calendars" style="visibility:hidden">
<div class="boxtitle"><roundcube:label name="calendar.calendars" /></div>
+ <div class="listsearchbox">
+ <div class="searchbox">
+ <input type="text" name="q" id="calendarlistsearch" placeholder="<roundcube:label name='calendar.findcalendars' />" />
+ <a class="iconbutton searchicon"></a>
+ <roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="reset searchreset" title="resetsearch" content="x" />
+ </div>
+ </div>
<div class="boxlistcontent">
- <roundcube:object name="plugin.calendar_list" id="calendarslist" />
+ <roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist" />
</div>
<div class="boxfooter">
<roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="buttonPas addgroup" classAct="button addgroup" content=" " />
commit 7c07ad1d427cd9c75fc4c40fe15c520b5b046273
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue May 20 09:50:41 2014 +0200
Display user's email address as hover title
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 3f74f0f..c845c15 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -113,6 +113,14 @@ class kolab_calendar extends kolab_storage_folder_api
return $this->name;
}
+ /**
+ *
+ */
+ public function get_title()
+ {
+ return null;
+ }
+
/**
* Return color to display this calendar
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index c07d74a..217fe42 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -151,6 +151,7 @@ class kolab_driver extends calendar_driver
'editname' => $cal->get_foldername(),
'color' => $cal->get_color(),
'active' => $cal->is_active(),
+ 'title' => $cal->get_owner(),
'owner' => $cal->get_owner(),
'virtual' => false,
'readonly' => true,
@@ -176,6 +177,7 @@ class kolab_driver extends calendar_driver
'name' => $fullname,
'listname' => $listname,
'editname' => $cal->get_foldername(),
+ 'title' => $cal->get_title(),
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
@@ -432,10 +434,8 @@ class kolab_driver extends calendar_driver
// search for calendar folders shared by this user
foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
- if (1 || !kolab_storage::folder_is_subscribed($foldername, true)) {
- $cal = new kolab_calendar($foldername, $this->cal);
- $this->calendars[$cal->id] = $cal;
- }
+ $cal = new kolab_calendar($foldername, $this->cal);
+ $this->calendars[$cal->id] = $cal;
}
}
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index 7fd74ee..1ab8679 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -90,6 +90,15 @@ class kolab_user_calendar extends kolab_calendar
/**
+ *
+ */
+ public function get_title()
+ {
+ return $this->userdata['mail'];
+ }
+
+
+ /**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @return string Name of the namespace (personal, other, shared)
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 74ea5d7..62b19f7 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -288,8 +288,8 @@ class calendar_ui
}
$classes = array('calendar', 'cal-' . asciiwords($id, true));
- $title = $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
- html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
+ $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+ html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
$is_collapsed = false; // TODO: determine this somehow?
if ($prop['virtual'])
commit 073a6bb3737e1f4c70593c7ce0f4b4f98699fdc0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon May 19 12:18:39 2014 +0200
Aggregate shared but unsubscribed calendar folders and free/busy data into a 'user calendar'
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index 4cf3dab..7fd74ee 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -30,6 +30,7 @@ class kolab_user_calendar extends kolab_calendar
public $subscriptions = false;
protected $userdata = array();
+ protected $timeindex = array();
/**
@@ -165,9 +166,158 @@ class kolab_user_calendar extends kolab_calendar
*/
public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
{
- // TODO: implement this
- console('kolab_user_calendar::list_events()');
- return array();
+ // convert to DateTime for comparisons
+ try {
+ $start_dt = new DateTime('@'.$start);
+ }
+ catch (Exception $e) {
+ $start_dt = new DateTime('@0');
+ }
+ try {
+ $end_dt = new DateTime('@'.$end);
+ }
+ catch (Exception $e) {
+ $end_dt = new DateTime('today +10 years');
+ }
+
+ $limit_changed = null;
+ if (!empty($query)) {
+ foreach ($query as $q) {
+ if ($q[0] == 'changed' && $q[1] == '>=') {
+ try { $limit_changed = new DateTime('@'.$q[2]); }
+ catch (Exception $e) { /* ignore */ }
+ }
+ }
+ }
+
+ // aggregate all calendar folders the user shares (but are not subscribed)
+ foreach (kolab_storage::list_user_folders($this->userdata, 'event', false) as $foldername) {
+ if (!kolab_storage::folder_is_subscribed($foldername, true)) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ foreach ($cal->list_events($start, $end, $search, 1) as $event) {
+ $this->events[$event['id']] = $event;
+ $this->timeindex[$this->time_key($event)] = $event['id'];
+ }
+ }
+ }
+
+ // get events from the user's free/busy feed
+ $this->fetch_freebusy($limit_changed);
+
+ $events = array();
+ foreach ($this->events as $id => $event) {
+ // list events in requested time window
+ if ($event['start'] <= $end_dt && $event['end'] >= $start_dt &&
+ (!$limit_changed || !$event['changed'] || $event['changed'] >= $limit_changed)) {
+ $events[] = $event;
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Helper method to fetch free/busy data for the user and turn it into calendar data
+ */
+ private function fetch_freebusy($limit_changed = null)
+ {
+ // ask kolab server first
+ try {
+ $request_config = array(
+ 'store_body' => true,
+ 'follow_redirects' => true,
+ );
+ $request = libkolab::http_request(kolab_storage::get_freebusy_url($this->userdata['mail']), 'GET', $request_config);
+ $response = $request->send();
+
+ // authentication required
+ if ($response->getStatus() == 401) {
+ $request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
+ $response = $request->send();
+ }
+
+ if ($response->getStatus() == 200)
+ $fbdata = $response->getBody();
+
+ unset($request, $response);
+ }
+ catch (Exception $e) {
+ rcube::raise_error(array(
+ 'code' => 900,
+ 'type' => 'php',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => "Error fetching free/busy information: " . $e->getMessage()),
+ true, false);
+
+ return false;
+ }
+
+ $statusmap = array(
+ 'FREE' => 'free',
+ 'BUSY' => 'busy',
+ 'BUSY-TENTATIVE' => 'tentative',
+ 'X-OUT-OF-OFFICE' => 'outofoffice',
+ 'OOF' => 'outofoffice',
+ );
+ $titlemap = array(
+ 'FREE' => $this->cal->gettext('availfree'),
+ 'BUSY' => $this->cal->gettext('availbusy'),
+ 'BUSY-TENTATIVE' => $this->cal->gettext('availtentative'),
+ 'X-OUT-OF-OFFICE' => $this->cal->gettext('availoutofoffice'),
+ );
+
+ // console('_fetch_freebusy', kolab_storage::get_freebusy_url($this->userdata['mail']), $fbdata);
+
+ // parse free-busy information using Horde classes
+ $count = 0;
+ if ($fbdata) {
+ $ical = $this->cal->get_ical();
+ $ical->import($fbdata);
+ if ($fb = $ical->freebusy) {
+ $result = array();
+
+ // consider 'changed >= X' queries
+ if ($limit_changed && $fb['created'] && $fb['created'] < $limit_changed) {
+ return 0;
+ }
+
+ foreach ($fb['periods'] as $tuple) {
+ list($from, $to, $type) = $tuple;
+ $event = array(
+ 'id' => md5($this->id . $from->format('U') . '/' . $to->format('U')),
+ 'calendar' => $this->id,
+ 'changed' => $fb['created'] ?: new DateTime(),
+ 'title' => $titlemap[$type] ?: $type,
+ 'start' => $from,
+ 'end' => $to,
+ 'free_busy' => $statusmap[$type] ?: 'busy',
+ 'organizer' => array(
+ 'email' => $this->userdata['mail'],
+ 'name' => $this->userdata['displayname'],
+ ),
+ );
+
+ // avoid duplicate entries
+ $key = $this->time_key($event);
+ if (!$this->timeindex[$key]) {
+ $this->events[$event['id']] = $event;
+ $this->timeindex[$key] = $event['id'];
+ $count++;
+ }
+ }
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * Helper to build a key for the absolute time slot the given event convers
+ */
+ private function time_key($event)
+ {
+ return sprintf('%s/%s', $event['start']->format('U'), is_object($event['end']->format('U')) ?: '0');
}
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 1dda548..3b0f159 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -673,9 +673,12 @@ class libvcalendar implements Iterator
continue;
switch ($prop->name) {
+ case 'CREATED':
+ case 'LAST-MODIFIED':
+ case 'DTSTAMP':
case 'DTSTART':
case 'DTEND':
- $propmap = array('DTSTART' => 'start', 'DTEND' => 'end');
+ $propmap = array('DTSTART' => 'start', 'DTEND' => 'end', 'CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed');
$this->freebusy[$propmap[$prop->name]] = self::convert_datetime($prop);
break;
commit fd3f93d64eee2495783d434411247270e99a2e36
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon May 19 12:16:14 2014 +0200
Use the dedicated config option
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 74c0ed3..4266fb5 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1340,7 +1340,7 @@ class kolab_storage
// resolve to IMAP folder name
$root = self::namespace_root('other');
- $user_attrib = self::$config->get('kolab_auth_login', 'mail');
+ $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
list($localpart, $domain) = explode('@', $user[$user_attrib]);
@@ -1368,7 +1368,7 @@ class kolab_storage
$folders = array();
// use localpart of user attribute as root for folder listing
- $user_attrib = self::$config->get('kolab_auth_login', 'mail');
+ $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
if (!empty($user[$user_attrib])) {
list($mbox) = explode('@', $user[$user_attrib]);
commit 2d85ff565f554577e4df8b30fdb08b79cacbb216
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri May 16 10:59:28 2014 +0200
Find next matching parent folder
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 8f1af0c..c07d74a 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -128,8 +128,13 @@ class kolab_driver extends calendar_driver
$fullname = $cal->get_name();
$listname = $cal->get_foldername();
$imap_path = explode('/', $cal->name);
- $topname = array_pop($imap_path);
- $parent_id = kolab_storage::folder_id(join('/', $imap_path));
+
+ // find parent
+ do {
+ array_pop($imap_path);
+ $parent_id = kolab_storage::folder_id(join('/', $imap_path));
+ }
+ while (count($imap_path) > 0 && !$this->calendars[$parent_id]);
// turn a kolab_storage_folder object into a kolab_calendar
if ($cal instanceof kolab_storage_folder) {
commit 0fbfff3349d487eac1e5a379c23e1728340d408b
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri May 16 10:38:37 2014 +0200
Limit the user search results and display message if list is truncated
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index f910b2b..15c81f9 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -746,6 +746,10 @@ class calendar extends rcube_plugin
$results[] = $cal;
}
+ // report more results available
+ if ($this->driver->search_more_results)
+ $this->rc->output->show_message('autocompletemore', 'info');
+
$this->rc->output->command('multi_thread_http_response', $results, get_input_value('_reqid', RCUBE_INPUT_GPC));
return;
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 8126339..8f1af0c 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -409,6 +409,7 @@ class kolab_driver extends calendar_driver
return array();
$this->calendars = array();
+ $this->search_more_results = false;
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
@@ -419,7 +420,8 @@ class kolab_driver extends calendar_driver
}
// find other user's virtual calendars
else if ($source == 'users') {
- foreach (kolab_storage::search_users($query, 0) as $user) {
+ $limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
+ foreach (kolab_storage::search_users($query, 0, array(), $limit, $count) as $user) {
$calendar = new kolab_user_calendar($user, $this->cal);
$this->calendars[$calendar->id] = $calendar;
@@ -431,6 +433,10 @@ class kolab_driver extends calendar_driver
}
}
}
+
+ if ($count > $limit) {
+ $this->search_more_results = true;
+ }
}
// don't list the birthday calendar
commit 7d5fe4c7942bb26b3c91353fbb19e71aea4320c4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri May 16 10:36:57 2014 +0200
Fix LDAP search calls and return the number of matches
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index 274d57f..00882a8 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -36,6 +36,7 @@ function kolab_folderlist(node, p)
var search_results_widget;
var search_results_container;
var listsearch_request;
+ var search_messagebox;
var Q = rcmail.quote_html;
@@ -164,8 +165,29 @@ function kolab_folderlist(node, p)
}
search_results = {};
+ if (search_messagebox)
+ rcmail.hide_message(search_messagebox);
+
// send search request(s) to server
if (search.query && search.execute) {
+ // require a minimum length for the search string
+ if (rcmail.env.autocomplete_min_length && search.query.length < rcmail.env.autocomplete_min_length) {
+ search_messagebox = rcmail.display_message(
+ rcmail.get_label('autocompletechars').replace('$min', rcmail.env.autocomplete_min_length));
+ return;
+ }
+
+ if (listsearch_request) {
+ // ignore, let the currently runnung sequest finish
+ if (listsearch_request.query == search.query) {
+ return;
+ }
+ else { // cancel previous search request
+ rcmail.multi_thread_request_abort(listsearch_request.id);
+ listsearch_request = null;
+ }
+ }
+
var sources = p.search_sources || [ 'folders' ];
var reqid = rcmail.multi_thread_http_request({
items: sources,
@@ -173,10 +195,15 @@ function kolab_folderlist(node, p)
action: p.search_action || 'listsearch',
postdata: { action:'search', q:search.query, source:'%s' },
lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
- onresponse: render_search_results
+ onresponse: render_search_results,
+ whendone: function(e){ listsearch_request = null; }
});
- listsearch_request = { id:reqid, sources:sources.slice(), num:sources.length };
+ listsearch_request = { id:reqid, query:search.query };
+ }
+ else if (!search.query && listsearch_request) {
+ rcmail.multi_thread_request_abort(listsearch_request.id);
+ listsearch_request = null;
}
});
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 1cbdd76..74c0ed3 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1318,11 +1318,12 @@ class kolab_storage
* @param mixed $query Search value (or array of field => value pairs)
* @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
* @param array $required List of fields that shall ot be empty
- * @param int $limit Number of records
+ * @param int $limit Maximum number of records
+ * @param int $count Returns the number of records found
*
* @return array List or false on error
*/
- public static function search_users($query, $mode = 1, $required = array(), $limit = 0)
+ public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
{
// requires a working LDAP setup
if (!self::ldap()) {
@@ -1330,7 +1331,12 @@ class kolab_storage
}
// search users using the configured attributes
- $results = self::$ldap->search(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit);
+ $results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count);
+
+ // exclude myself
+ if ($_SESSION['kolab_dn']) {
+ unset($results[$_SESSION['kolab_dn']]);
+ }
// resolve to IMAP folder name
$root = self::namespace_root('other');
@@ -1391,6 +1397,9 @@ class kolab_storage
$path_len = count(explode($delimiter, $other_ns));
foreach ((array)self::list_folders($other_ns, '*', '', $subscribed) as $foldername) {
+ if ($foldername == 'INBOX') // skip INBOX which is added by default
+ continue;
+
// truncate folder path to top-level folders of the 'other' namespace
$path = explode($delimiter, $foldername);
$foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
commit 115c4c54b75791d80f7ab620003f89f4b12501fb
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri May 16 10:34:32 2014 +0200
Rename kolab_auth_ldap::search() method because its signature doesn't match rcube_ldap_generic::search() and fails recursive calls in VLV search mode + add return parameter for results count
diff --git a/plugins/kolab_auth/kolab_auth_ldap.php b/plugins/kolab_auth/kolab_auth_ldap.php
index 7044ebf..40237ac 100644
--- a/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/plugins/kolab_auth/kolab_auth_ldap.php
@@ -215,10 +215,11 @@ class kolab_auth_ldap extends rcube_ldap_generic
* 2 - prefix (abc*)
* @param array $required List of fields that cannot be empty
* @param int $limit Number of records
+ * @param int $count Returns the number of records found
*
* @return array List or false on error
*/
- function search($fields, $value, $mode=1, $required = array(), $limit = 0)
+ function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0)
{
if (empty($fields)) {
return array();
@@ -295,7 +296,8 @@ class kolab_auth_ldap extends rcube_ldap_generic
$attrs = array_values($this->fieldmap);
$list = array();
- if ($result = parent::search($base_dn, $filter, $scope, $attrs)) {
+ if ($result = $this->search($base_dn, $filter, $scope, $attrs)) {
+ $count = $result->count();
$i = 0;
foreach ($result as $entry) {
if ($limit && $limit <= $i) {
diff --git a/plugins/kolab_delegation/kolab_delegation_engine.php b/plugins/kolab_delegation/kolab_delegation_engine.php
index ad96831..3d3bd33 100644
--- a/plugins/kolab_delegation/kolab_delegation_engine.php
+++ b/plugins/kolab_delegation/kolab_delegation_engine.php
@@ -202,7 +202,7 @@ class kolab_delegation_engine
return array();
}
- $list = $ldap->search($this->ldap_login_field, $login, 1);
+ $list = $ldap->dosearch($this->ldap_login_field, $login, 1);
if (count($list) == 1) {
$dn = key($list);
@@ -288,7 +288,7 @@ class kolab_delegation_engine
return array();
}
- $list = $ldap->search($this->ldap_delegate_field, $this->ldap_dn, 1);
+ $list = $ldap->dosearch($this->ldap_delegate_field, $this->ldap_dn, 1);
foreach ($list as $dn => $delegator) {
$delegator = $this->parse_ldap_record($delegator, $dn);
@@ -424,7 +424,7 @@ class kolab_delegation_engine
$fields = array_unique(array_filter(array_merge((array)$this->ldap_name_field, (array)$this->ldap_login_field)));
$users = array();
- $result = $ldap->search($fields, $search, $mode, (array)$this->ldap_login_field, $max);
+ $result = $ldap->dosearch($fields, $search, $mode, (array)$this->ldap_login_field, $max);
foreach ($result as $record) {
// skip self
commit b415c512f00e8b60ddc079cd65fd610f4b612f16
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu May 15 17:20:58 2014 +0200
Use folder namespace for grouping in client-side treelist
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index d0e6b7b..3b833ab 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -138,7 +138,7 @@ class database_driver extends calendar_driver
'color' => $prefs['color'],
'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
'active' => !in_array($id, $hidden),
- 'class_name' => 'birthdays',
+ 'group' => 'birthdays',
'readonly' => true,
'default' => false,
'children' => false,
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index ff8372b..8126339 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -149,7 +149,8 @@ class kolab_driver extends calendar_driver
'owner' => $cal->get_owner(),
'virtual' => false,
'readonly' => true,
- 'class_name' => 'user',
+ 'group' => 'other',
+ 'class' => 'user',
);
}
else if ($cal->virtual) {
@@ -160,6 +161,8 @@ class kolab_driver extends calendar_driver
'editname' => $cal->get_foldername(),
'virtual' => true,
'readonly' => true,
+ 'group' => $cal->get_namespace(),
+ 'class' => 'folder',
);
}
else {
@@ -171,7 +174,7 @@ class kolab_driver extends calendar_driver
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
- 'class_name' => $cal->get_namespace(),
+ 'group' => $cal->get_namespace(),
'default' => $cal->default,
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
@@ -198,7 +201,7 @@ class kolab_driver extends calendar_driver
'color' => $prefs[$id]['color'],
'active' => $prefs[$id]['active'],
'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
- 'class_name' => 'birthdays',
+ 'group' => 'birthdays',
'readonly' => true,
'default' => false,
'children' => false,
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index a41e065..4cf3dab 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -56,6 +56,7 @@ class kolab_user_calendar extends kolab_calendar
$this->id = kolab_storage::folder_id($this->userdata['kolabtargetfolder'], true);
$this->imap_folder = $this->userdata['kolabtargetfolder'];
$this->name = $this->storage->get_name();
+ $this->parent = ''; // user calendars are top level
// user-specific alarms settings win
$prefs = $this->cal->rc->config->get('kolab_calendars', array());
@@ -94,7 +95,7 @@ class kolab_user_calendar extends kolab_calendar
*/
public function get_namespace()
{
- return 'user';
+ return 'other user';
}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 4d41194..74ea5d7 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -229,7 +229,7 @@ class calendar_ui
if ($attrib['activeonly'] && !$prop['active'])
continue;
- $html .= html::tag('li', array('id' => 'rcmlical' . $id),
+ $html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']),
$content = $this->calendar_list_item($id, $prop, $jsenv)
);
}
@@ -260,7 +260,7 @@ class calendar_ui
if (strlen($content)) {
$out .= html::tag('li', array(
'id' => 'rcmlical' . rcube_utils::html_identifier($id),
- 'class' => $prop['virtual'] ? 'virtual' : '',
+ 'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
),
$content);
}
@@ -287,23 +287,23 @@ class calendar_ui
$jsenv[$id] = $prop;
}
- $class = 'calendar cal-' . asciiwords($id, true);
+ $classes = array('calendar', 'cal-' . asciiwords($id, true));
$title = $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
$is_collapsed = false; // TODO: determine this somehow?
if ($prop['virtual'])
- $class = 'folder virtual';
+ $classes[] = 'virtual';
else if ($prop['readonly'])
- $class .= ' readonly';
+ $classes[] = 'readonly';
if ($prop['subscribed'])
- $class .= ' subscribed';
- if ($prop['class_name'])
- $class .= ' '.$prop['class_name'];
+ $classes[] = ' subscribed';
+ if ($prop['class'])
+ $classes[] = $prop['class'];
$content = '';
if (!$attrib['activeonly'] || $prop['active']) {
- $content = html::div($class,
+ $content = html::div(join(' ', $classes),
html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
($prop['virtual'] ? '' :
html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 67675bf..b92f377 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -86,8 +86,9 @@ $labels['nmonthsback'] = '$nr months back';
$labels['showurl'] = 'Show calendar URL';
$labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
+$labels['findcalendars'] = 'Find calendars...';
$labels['calsearchresults'] = 'Available Calendars';
-$labels['calendarsubscribe'] = 'Listed permanently';
+$labels['calendarsubscribe'] = 'List permanently';
// agenda view
$labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index cca70e6..5ef63f7 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -293,7 +293,7 @@ pre {
background-position: right -20px;
}
-#calendars .treelist div.user span.calname {
+#calendars .treelist li.user > div > span.calname {
background-position: right -38px;
}
/*
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 0842cf0..7f9f0af 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -25,7 +25,7 @@
<h2 class="boxtitle"><roundcube:label name="calendar.calendars" /></h2>
<div class="listsearchbox">
<div class="searchbox">
- <input type="text" name="q" id="calendarlistsearch" />
+ <input type="text" name="q" id="calendarlistsearch" placeholder="<roundcube:label name='calendar.findcalendars' />" />
<a class="iconbutton searchicon"></a>
<roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
</div>
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index 587fe56..274d57f 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -49,10 +49,12 @@ function kolab_folderlist(node, p)
.html(p.search_title ? '<h2 class="boxtitle">' + p.search_title + '</h2>' : '')
.insertAfter(me.container);
- search_results_widget = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
+ search_results_widget = new rcube_treelist_widget('<ul>', {
id_prefix: p.id_prefix,
selectable: false
});
+ // copy classes from main list
+ search_results_widget.container.addClass(me.container.attr('class'));
// register click handler on search result's checkboxes to select the given item for listing
search_results_widget.container
@@ -94,7 +96,7 @@ function kolab_folderlist(node, p)
search_results[prop.id] = prop;
search_results_widget.insert({
id: prop.id,
- classes: prop.class_name ? String(prop.class_name).split(' ') : [],
+ classes: [ prop.group || '' ],
html: item,
collapsed: true
}, prop.parent);
@@ -142,10 +144,10 @@ function kolab_folderlist(node, p)
// move this result item to the main list widget
me.insert({
id: id,
- classes: [],
+ classes: [ prop.group || '' ],
virtual: prop.virtual,
html: dom_node,
- }, parent_id, parent_id ? true : false);
+ }, parent_id, prop.group);
}
delete prop.html;
commit 857078428b952de8aa8537ec1e2238198ab50f12
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu May 15 15:53:35 2014 +0200
Toggle IMAP subscriptions directly from the calendars/folders list (#3042)
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index fc0069d..f910b2b 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -725,7 +725,7 @@ class calendar extends rcube_plugin
$this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
break;
case "subscribe":
- if (!$this->driver->subscribe_calendar($cal, intval(get_input_value('perm', RCUBE_INPUT_GPC))))
+ if (!$this->driver->subscribe_calendar($cal))
$this->rc->output->show_message($this->gettext('errorsaving'), 'error');
return;
case "search":
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 38a32a8..68ae94f 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2697,9 +2697,14 @@ function rcube_calendar_ui(settings)
me.calendars[id].color = color;
}
- if (fc && cal.active) {
- fc.fullCalendar('addEventSource', me.calendars[id]);
- rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:cal.active?1:0 } });
+ if (fc && (cal.active || cal.subscribed)) {
+ if (cal.active)
+ fc.fullCalendar('addEventSource', me.calendars[id]);
+
+ var submit = { id: id, active: cal.active ? 1 : 0 };
+ if (cal.subscribed !== undefined)
+ submit.permanent = cal.subscribed ? 1 : 0;
+ rcmail.http_post('calendar', { action:'subscribe', c:submit });
}
// insert to #calendar-select options if writeable
@@ -2761,6 +2766,13 @@ function rcube_calendar_ui(settings)
}
}
});
+ calendars_list.addEventListener('subscribe', function(p) {
+ var cal;
+ if ((cal = me.calendars[p.id])) {
+ cal.subscribed = p.subscribed || false;
+ rcmail.http_post('calendar', { action:'subscribe', c:{ id:p.id, active:cal.active?1:0, permanent:cal.subscribed?1:0 } });
+ }
+ });
// init (delegate) event handler on calendar list checkboxes
$(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e){
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index c4b7dbb..3f74f0f 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -30,6 +30,7 @@ class kolab_calendar extends kolab_storage_folder_api
public $readonly = true;
public $attachments = true;
public $alarms = false;
+ public $subscriptions = true;
public $categories = array();
public $storage;
@@ -103,19 +104,6 @@ class kolab_calendar extends kolab_storage_folder_api
/**
- * Getter for a nice and human readable name for this calendar
- * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
- *
- * @return string Name of this calendar
- */
- public function get_name()
- {
- $folder = kolab_storage::object_name($this->name, $this->namespace);
- return $folder;
- }
-
-
- /**
* Getter for the IMAP folder name
*
* @return string Name of the IMAP folder
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index d1bb655..ff8372b 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -180,6 +180,10 @@ class kolab_driver extends calendar_driver
'caldavurl' => $cal->get_caldav_url(),
);
}
+
+ if ($cal->subscriptions) {
+ $calendars[$cal->id]['subscribed'] = (bool)$cal->is_subscribed();
+ }
}
// append the virtual birthdays calendar
@@ -258,7 +262,6 @@ class kolab_driver extends calendar_driver
// create calendar object if necesary
if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
$calendar = kolab_calendar::factory($id, $this->cal);
- console($id, $calendar->id, $calendar->ready);
if ($calendar->ready)
$this->calendars[$calendar->id] = $calendar;
}
@@ -342,11 +345,15 @@ class kolab_driver extends calendar_driver
*
* @see calendar_driver::subscribe_calendar()
*/
- public function subscribe_calendar($prop, $permanent = false)
+ public function subscribe_calendar($prop)
{
if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
- if ($permanent) $cal->storage->subscribe($prop['active']);
- return $cal->storage->activate($prop['active']);
+ $ret = false;
+ if (isset($prop['permanent']))
+ $ret |= $cal->storage->subscribe($prop['permanent']);
+ if (isset($prop['active']))
+ $ret |= $cal->storage->activate($prop['active']);
+ return $ret;
}
else {
// save state in local prefs
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index 3add82a..a41e065 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -27,6 +27,7 @@ class kolab_user_calendar extends kolab_calendar
public $ready = false;
public $readonly = true;
public $attachments = false;
+ public $subscriptions = false;
protected $userdata = array();
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 4584f19..4d41194 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -296,6 +296,8 @@ class calendar_ui
$class = 'folder virtual';
else if ($prop['readonly'])
$class .= ' readonly';
+ if ($prop['subscribed'])
+ $class .= ' subscribed';
if ($prop['class_name'])
$class .= ' '.$prop['class_name'];
@@ -303,8 +305,11 @@ class calendar_ui
if (!$attrib['activeonly'] || $prop['active']) {
$content = html::div($class,
html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
- ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
- html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' '))
+ ($prop['virtual'] ? '' :
+ html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
+ (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), $this->cal->gettext('subscribed')) : '') .
+ html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')
+ )
);
}
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 92b4fb5..67675bf 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -87,6 +87,7 @@ $labels['showurl'] = 'Show calendar URL';
$labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
$labels['calsearchresults'] = 'Available Calendars';
+$labels['calendarsubscribe'] = 'Listed permanently';
// agenda view
$labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 71dd106..cca70e6 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -190,7 +190,7 @@ pre {
position: absolute;
top: 7px;
left: 38px;
- right: 22px;
+ right: 40px;
cursor: default;
background: url(images/calendars.png) right 20px no-repeat;
overflow: hidden;
@@ -207,6 +207,7 @@ pre {
#calendars .treelist.flat li span.calname {
left: 24px;
+ right: 22px;
}
#calendars .treelist li span.handle {
@@ -225,6 +226,28 @@ pre {
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
}
+#calendars .treelist li a.subscribed {
+ display: inline-block;
+ position: absolute;
+ top: 7px;
+ right: 24px;
+ height: 16px;
+ width: 16px;
+ padding: 0;
+ background: url(images/calendars.png) -100px 0 no-repeat;
+ overflow: hidden;
+ text-indent: -5000px;
+ cursor: pointer;
+}
+
+#calendars .treelist div:hover > a.subscribed {
+ background-position: 1px -110px;
+}
+
+#calendars .treelist div.subscribed a.subscribed {
+ background-position: -15px -110px;
+}
+
#calendars .treelist li input {
position: absolute;
top: 5px;
diff --git a/plugins/calendar/skins/larry/images/calendars.png b/plugins/calendar/skins/larry/images/calendars.png
index c2de67d..1f97abc 100644
Binary files a/plugins/calendar/skins/larry/images/calendars.png and b/plugins/calendar/skins/larry/images/calendars.png differ
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index 3c35846..587fe56 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -57,20 +57,29 @@ function kolab_folderlist(node, p)
// register click handler on search result's checkboxes to select the given item for listing
search_results_widget.container
.appendTo(search_results_container)
- .on('click', 'input[type=checkbox]', function(e) {
- if (!this.checked)
- return;
-
+ .on('click', 'input[type=checkbox], a.subscribed', function(e) {
var li = $(this).closest('li'),
id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '')
node = search_results_widget.get_node(id),
has_children = node.children && node.children.length;
+ // activate + subscribe
+ if ($(e.target).hasClass('subscribed')) {
+ search_results[id].subscribed = true;
+ li.children().first()
+ .toggleClass('subscribed')
+ .find('input[type=checkbox]').get(0).checked = true;
+ }
+ else if (!this.checked) {
+ return;
+ }
+
// copy item to the main list
add_result2list(id, li, true);
if (has_children) {
li.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+ li.find('a.subscribed').first().hide();
}
else {
li.remove();
@@ -93,6 +102,7 @@ function kolab_folderlist(node, p)
// disable checkbox if item already exists in main list
if (me.get_node(prop.id) && !me.get_node(prop.id).virtual) {
item.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+ item.find('a.subscribed').hide();
}
}
@@ -168,6 +178,18 @@ function kolab_folderlist(node, p)
}
});
+ this.container.on('click', 'a.subscribed', function(e){
+ var li = $(this).closest('li'),
+ id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+ div = li.children().first();
+
+ div.toggleClass('subscribed');
+ me.triggerEvent('subscribe', { id: id, subscribed: div.hasClass('subscribed'), item: li });
+
+ e.stopPropagation();
+ return false;
+ })
+
}
// link prototype from base class
commit 8d09b78eb1ee58ea19180804ff729000a82f6340
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu May 15 14:18:23 2014 +0200
Provide sample config for new LDAP user search
diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist
index e1ad0ff..3a3c287 100644
--- a/plugins/libkolab/config.inc.php.dist
+++ b/plugins/libkolab/config.inc.php.dist
@@ -31,3 +31,20 @@ $rcmail_config['kolab_http_request'] = array();
// 1 - bypass only messages, but use index cache
$rcmail_config['kolab_messages_cache_bypass'] = 0;
+// LDAP directory to find avilable users for folder sharing.
+// Either contains an array with LDAP addressbook configuration or refers to entry in $config['ldap_public'].
+// If not specified, the configuraton from 'kolab_auth_addressbook' will be used.
+$rcmail_config['kolab_users_directory'] = null;
+
+// Filter to be used for resolving user folders in LDAP.
+// Defaults to the 'kolab_auth_filter' configuration option.
+$rcmail_config['kolab_users_filter'] = '(&(objectclass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)))';
+
+// Which property of the LDAP user record to use for user folder mapping in IMAP.
+// Defaults to the 'kolab_auth_login' configuration option.
+$rcmail_config['kolab_users_id_attrib'] = null;
+
+// Use these attributes when searching users in LDAP
+$rcmail_config['kolab_users_search_attrib'] = array('cn','mail','alias');
+
+
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index f10b7fe..1cbdd76 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -111,11 +111,12 @@ class kolab_storage
return self::$ldap;
}
- $rcmail = rcube::get_instance();
- $config = $rcmail->config->get('kolab_users_directory', $rcmail->config->get('kolab_auth_addressbook'));
+ self::setup();
+
+ $config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook'));
if (!is_array($config)) {
- $ldap_config = (array)$rcmail->config->get('ldap_public');
+ $ldap_config = (array)self::$config->get('ldap_public');
$config = $ldap_config[$config];
}
@@ -124,8 +125,8 @@ class kolab_storage
}
// overwrite filter option
- if ($filter = $rcmail->config->get('kolab_users_filter')) {
- $rcmail->config->set('kolab_auth_filter', $filter);
+ if ($filter = self::$config->get('kolab_users_filter')) {
+ self::$config->set('kolab_auth_filter', $filter);
}
// re-use the LDAP wrapper class from kolab_auth plugin
@@ -1328,12 +1329,12 @@ class kolab_storage
return array();
}
- // FIXME: make search attributes configurable
- $results = self::$ldap->search(array('cn','mail','alias'), $query, $mode, $required, $limit);
+ // search users using the configured attributes
+ $results = self::$ldap->search(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit);
// resolve to IMAP folder name
$root = self::namespace_root('other');
- $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+ $user_attrib = self::$config->get('kolab_auth_login', 'mail');
array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
list($localpart, $domain) = explode('@', $user[$user_attrib]);
@@ -1356,10 +1357,12 @@ class kolab_storage
*/
public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
{
+ self::setup();
+
$folders = array();
// use localpart of user attribute as root for folder listing
- $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+ $user_attrib = self::$config->get('kolab_auth_login', 'mail');
if (!empty($user[$user_attrib])) {
list($mbox) = explode('@', $user[$user_attrib]);
diff --git a/plugins/libkolab/lib/kolab_storage_folder_api.php b/plugins/libkolab/lib/kolab_storage_folder_api.php
index a2d40b1..5af8c34 100644
--- a/plugins/libkolab/lib/kolab_storage_folder_api.php
+++ b/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -134,7 +134,7 @@ abstract class kolab_storage_folder_api
*/
public function get_name()
{
- return kolab_storage::object_name($this->name, $this->namespace);
+ return kolab_storage::object_name($this->name, $this->get_namespace());
}
diff --git a/plugins/libkolab/lib/kolab_storage_folder_virtual.php b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
index 8b85ad5..e419ced 100644
--- a/plugins/libkolab/lib/kolab_storage_folder_virtual.php
+++ b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
@@ -43,8 +43,7 @@ class kolab_storage_folder_virtual extends kolab_storage_folder_api
*/
public function get_name()
{
- // this is already kolab_storage::object_name() result
- return $this->displayname;
+ return $this->displayname ?: parent::get_name();
}
/**
commit 510089523ee8753733e0678e93760baa15441a23
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu May 15 13:15:58 2014 +0200
Refactored kolab_storage_folder classes and consolidated some functions
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 374ab5d..c4b7dbb 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -24,9 +24,8 @@
*/
-class kolab_calendar
+class kolab_calendar extends kolab_storage_folder_api
{
- public $id;
public $ready = false;
public $readonly = true;
public $attachments = true;
@@ -35,11 +34,9 @@ class kolab_calendar
public $storage;
public $type = 'event';
- public $name;
protected $cal;
protected $events = array();
- protected $imap_folder = 'INBOX/Calendar';
protected $search_fields = array('title', 'description', 'location', 'attendees');
/**
@@ -68,16 +65,15 @@ class kolab_calendar
public function __construct($imap_folder, $calendar)
{
$this->cal = $calendar;
-
- if (strlen($imap_folder))
- $this->imap_folder = $this->name = $imap_folder;
+ $this->imap = $calendar->rc->get_storage();
+ $this->name = $imap_folder;
// ID is derrived from folder name
- $this->id = kolab_storage::folder_id($this->imap_folder, true);
- $old_id = kolab_storage::folder_id($this->imap_folder, false);
+ $this->id = kolab_storage::folder_id($this->name, true);
+ $old_id = kolab_storage::folder_id($this->name, false);
// fetch objects from the given IMAP folder
- $this->storage = kolab_storage::get_folder($this->imap_folder);
+ $this->storage = kolab_storage::get_folder($this->name);
$this->ready = $this->storage && !PEAR::isError($this->storage) && $this->storage->type !== null;
// Set readonly and alarms flags according to folder permissions
@@ -101,6 +97,8 @@ class kolab_calendar
else if (isset($prefs[$old_id]['showalarms']))
$this->alarms = $prefs[$old_id]['showalarms'];
}
+
+ $this->default = $this->storage->default;
}
@@ -112,7 +110,7 @@ class kolab_calendar
*/
public function get_name()
{
- $folder = kolab_storage::object_name($this->imap_folder, $this->namespace);
+ $folder = kolab_storage::object_name($this->name, $this->namespace);
return $folder;
}
@@ -124,44 +122,11 @@ class kolab_calendar
*/
public function get_realname()
{
- return $this->imap_folder;
- }
-
-
- /**
- * Getter for the IMAP folder owner
- *
- * @return string Name of the folder owner
- */
- public function get_owner()
- {
- return $this->storage->get_owner();
+ return $this->name;
}
/**
- * Getter for the name of the namespace to which the IMAP folder belongs
- *
- * @return string Name of the namespace (personal, other, shared)
- */
- public function get_namespace()
- {
- return $this->storage->get_namespace();
- }
-
-
- /**
- * Getter for the top-end calendar folder name (not the entire path)
- *
- * @return string Name of this calendar
- */
- public function get_foldername()
- {
- $parts = explode('/', $this->imap_folder);
- return rcube_charset::convert(end($parts), 'UTF7-IMAP');
- }
-
- /**
* Return color to display this calendar
*/
public function get_color()
@@ -191,7 +156,7 @@ class kolab_calendar
'%h' => $_SERVER['HTTP_HOST'],
'%u' => urlencode($this->cal->rc->get_user_name()),
'%i' => urlencode($this->storage->get_uid()),
- '%n' => urlencode($this->imap_folder),
+ '%n' => urlencode($this->name),
));
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 13c460e..d1bb655 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -83,7 +83,7 @@ class kolab_driver extends calendar_driver
$this->calendars = array();
foreach ($folders as $folder) {
- if ($folder instanceof kolab_storage_user_folder)
+ if ($folder instanceof kolab_storage_folder_user)
$calendar = new kolab_user_calendar($folder->name, $this->cal);
else
$calendar = new kolab_calendar($folder->name, $this->cal);
@@ -138,7 +138,7 @@ class kolab_driver extends calendar_driver
}
// special handling for user or virtual folders
- if ($cal instanceof kolab_storage_user_folder) {
+ if ($cal instanceof kolab_storage_folder_user) {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => kolab_storage::object_name($fullname),
@@ -172,8 +172,8 @@ class kolab_driver extends calendar_driver
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'class_name' => $cal->get_namespace(),
- 'default' => $cal->storage->default,
- 'active' => $cal->storage->is_active(),
+ 'default' => $cal->default,
+ 'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
'children' => true, // TODO: determine if that folder indeed has child folders
'parent' => $parent_id,
@@ -234,7 +234,7 @@ class kolab_driver extends calendar_driver
if ($writeable && $cal->readonly) {
continue;
}
- if ($active && !$cal->storage->is_active()) {
+ if ($active && !$cal->is_active()) {
continue;
}
if ($personal && $cal->get_namespace() != 'personal') {
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index b23d134..3add82a 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -27,7 +27,6 @@ class kolab_user_calendar extends kolab_calendar
public $ready = false;
public $readonly = true;
public $attachments = false;
- public $name;
protected $userdata = array();
@@ -42,10 +41,10 @@ class kolab_user_calendar extends kolab_calendar
// full user record is provided
if (is_array($user_or_folder)) {
$this->userdata = $user_or_folder;
- $this->storage = new kolab_storage_user_folder($this->userdata['kolabtargetfolder'], '', $this->userdata);
+ $this->storage = new kolab_storage_folder_user($this->userdata['kolabtargetfolder'], '', $this->userdata);
}
else { // get user record from LDAP
- $this->storage = new kolab_storage_user_folder($user_or_folder);
+ $this->storage = new kolab_storage_folder_user($user_or_folder);
$this->userdata = $this->storage->ldaprec;
}
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 17c4912..f10b7fe 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -887,7 +887,7 @@ class kolab_storage
$_folders = array();
$delim = self::$imap->get_hierarchy_delimiter();
$other_ns = rtrim(self::namespace_root('other'), $delim);
- $tree = new kolab_storage_virtual_folder('', '<root>', ''); // create tree root
+ $tree = new kolab_storage_folder_virtual('', '<root>', ''); // create tree root
$refs = array('' => $tree);
foreach ($folders as $idx => $folder) {
@@ -913,11 +913,11 @@ class kolab_storage
$refs[$parent]->parent = $parent_parent;
}
else if ($parent_parent == $other_ns) {
- $refs[$parent] = new kolab_storage_user_folder($parent, $parent_parent);
+ $refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
}
else {
$name = kolab_storage::object_name($parent, $folder->get_namespace());
- $refs[$parent] = new kolab_storage_virtual_folder($parent, $name, $folder->get_namespace(), $parent_parent);
+ $refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
}
$parents[] = $refs[$parent];
}
@@ -1376,7 +1376,7 @@ class kolab_storage
*
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
- * @return array List of kolab_storage_user_folder objects
+ * @return array List of kolab_storage_folder_user objects
*/
public static function get_user_folders($subscribed)
{
@@ -1393,7 +1393,7 @@ class kolab_storage
$foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
if (!$folders[$foldername]) {
- $folders[$foldername] = new kolab_storage_user_folder($foldername, $other_ns);
+ $folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
}
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 39d1964..db1a761 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -22,50 +22,15 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-class kolab_storage_folder
+class kolab_storage_folder extends kolab_storage_folder_api
{
/**
- * Folder identifier
- * @var string
- */
- public $id;
-
- /**
- * The folder name.
- * @var string
- */
- public $name;
-
- /**
- * The type of this folder.
- * @var string
- */
- public $type;
-
- /**
- * Is this folder set to be the default for its type
- * @var boolean
- */
- public $default = false;
-
- /**
* The kolab_storage_cache instance for caching operations
* @var object
*/
public $cache;
-
- /**
- * List of direct child folders
- * @var array
- */
- public $children = array();
private $type_annotation;
- private $namespace;
- private $imap;
- private $info;
- private $idata;
- private $owner;
private $resource_uri;
@@ -74,7 +39,7 @@ class kolab_storage_folder
*/
function __construct($name, $type = null)
{
- $this->imap = rcube::get_instance()->get_storage();
+ parent::__construct($name);
$this->imap->set_options(array('skip_deleted' => true));
$this->set_folder($name, $type);
}
@@ -105,160 +70,6 @@ class kolab_storage_folder
$this->cache->set_folder($this);
}
- /**
- *
- */
- public function get_folder_info()
- {
- if (!isset($this->info))
- $this->info = $this->imap->folder_info($this->name);
-
- return $this->info;
- }
-
- /**
- * Make IMAP folder data available for this folder
- */
- public function get_imap_data()
- {
- if (!isset($this->idata))
- $this->idata = $this->imap->folder_data($this->name);
-
- return $this->idata;
- }
-
- /**
- * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
- *
- * @param array List of metadata keys to read
- * @return array Metadata entry-value hash array on success, NULL on error
- */
- public function get_metadata($keys)
- {
- $metadata = $this->imap->get_metadata($this->name, (array)$keys);
- return $metadata[$this->name];
- }
-
-
- /**
- * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
- *
- * @param array $entries Entry-value array (use NULL value as NIL)
- * @return boolean True on success, False on failure
- */
- public function set_metadata($entries)
- {
- return $this->imap->set_metadata($this->name, $entries);
- }
-
-
- /**
- * Returns the owner of the folder.
- *
- * @return string The owner of this folder.
- */
- public function get_owner()
- {
- // return cached value
- if (isset($this->owner))
- return $this->owner;
-
- $info = $this->get_folder_info();
- $rcmail = rcube::get_instance();
-
- switch ($info['namespace']) {
- case 'personal':
- $this->owner = $rcmail->get_user_name();
- break;
-
- case 'shared':
- $this->owner = 'anonymous';
- break;
-
- default:
- list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
- if (strpos($user, '@') === false) {
- $domain = strstr($rcmail->get_user_name(), '@');
- if (!empty($domain))
- $user .= $domain;
- }
- $this->owner = $user;
- break;
- }
-
- return $this->owner;
- }
-
-
- /**
- * Getter for the name of the namespace to which the IMAP folder belongs
- *
- * @return string Name of the namespace (personal, other, shared)
- */
- public function get_namespace()
- {
- if (!isset($this->namespace))
- $this->namespace = $this->imap->folder_namespace($this->name);
- return $this->namespace;
- }
-
-
- /**
- * Get IMAP ACL information for this folder
- *
- * @return string Permissions as string
- */
- public function get_myrights()
- {
- $rights = $this->info['rights'];
-
- if (!is_array($rights))
- $rights = $this->imap->my_rights($this->name);
-
- return join('', (array)$rights);
- }
-
-
- /**
- * Get the display name value of this folder
- *
- * @return string Folder name
- */
- public function get_name()
- {
- return kolab_storage::object_name($this->name, $this->namespace);
- }
-
-
- /**
- * Getter for the top-end folder name (not the entire path)
- *
- * @return string Name of this folder
- */
- public function get_foldername()
- {
- $parts = explode('/', $this->name);
- return rcube_charset::convert(end($parts), 'UTF7-IMAP');
- }
-
-
- /**
- * Get the color value stored in metadata
- *
- * @param string Default color value to return if not set
- * @return mixed Color value from IMAP metadata or $default is not set
- */
- public function get_color($default = null)
- {
- // color is defined in folder METADATA
- $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
- if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
- return $color;
- }
-
- return $default;
- }
-
/**
* Compose a unique resource URI for this IMAP folder
diff --git a/plugins/libkolab/lib/kolab_storage_folder_api.php b/plugins/libkolab/lib/kolab_storage_folder_api.php
new file mode 100644
index 0000000..a2d40b1
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -0,0 +1,280 @@
+<?php
+
+/**
+ * Abstract interface class for Kolab storage IMAP folder objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+abstract class kolab_storage_folder_api
+{
+ /**
+ * Folder identifier
+ * @var string
+ */
+ public $id;
+
+ /**
+ * The folder name.
+ * @var string
+ */
+ public $name;
+
+ /**
+ * The type of this folder.
+ * @var string
+ */
+ public $type;
+
+ /**
+ * Is this folder set to be the default for its type
+ * @var boolean
+ */
+ public $default = false;
+
+ /**
+ * List of direct child folders
+ * @var array
+ */
+ public $children = array();
+
+ /**
+ * Name of the parent folder
+ * @var string
+ */
+ public $parent = '';
+
+ protected $imap;
+ protected $owner;
+ protected $info;
+ protected $idata;
+ protected $namespace;
+
+
+ /**
+ * Private constructor
+ */
+ protected function __construct($name)
+ {
+ $this->name = $name;
+ $this->id = kolab_storage::folder_id($name);
+ $this->imap = rcube::get_instance()->get_storage();
+ }
+
+
+ /**
+ * Returns the owner of the folder.
+ *
+ * @return string The owner of this folder.
+ */
+ public function get_owner()
+ {
+ // return cached value
+ if (isset($this->owner))
+ return $this->owner;
+
+ $info = $this->get_folder_info();
+ $rcmail = rcube::get_instance();
+
+ switch ($info['namespace']) {
+ case 'personal':
+ $this->owner = $rcmail->get_user_name();
+ break;
+
+ case 'shared':
+ $this->owner = 'anonymous';
+ break;
+
+ default:
+ list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+ if (strpos($user, '@') === false) {
+ $domain = strstr($rcmail->get_user_name(), '@');
+ if (!empty($domain))
+ $user .= $domain;
+ }
+ $this->owner = $user;
+ break;
+ }
+
+ return $this->owner;
+ }
+
+
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ if (!isset($this->namespace))
+ $this->namespace = $this->imap->folder_namespace($this->name);
+ return $this->namespace;
+ }
+
+
+ /**
+ * Get the display name value of this folder
+ *
+ * @return string Folder name
+ */
+ public function get_name()
+ {
+ return kolab_storage::object_name($this->name, $this->namespace);
+ }
+
+
+ /**
+ * Getter for the top-end folder name (not the entire path)
+ *
+ * @return string Name of this folder
+ */
+ public function get_foldername()
+ {
+ $parts = explode('/', $this->name);
+ return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+ }
+
+
+ /**
+ * Get the color value stored in metadata
+ *
+ * @param string Default color value to return if not set
+ * @return mixed Color value from IMAP metadata or $default is not set
+ */
+ public function get_color($default = null)
+ {
+ // color is defined in folder METADATA
+ $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
+ if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
+ return $color;
+ }
+
+ return $default;
+ }
+
+
+ /**
+ * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
+ *
+ * @param array List of metadata keys to read
+ * @return array Metadata entry-value hash array on success, NULL on error
+ */
+ public function get_metadata($keys)
+ {
+ $metadata = rcube::get_instance()->get_storage()->get_metadata($this->name, (array)$keys);
+ return $metadata[$this->name];
+ }
+
+
+ /**
+ * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+ *
+ * @param array $entries Entry-value array (use NULL value as NIL)
+ * @return boolean True on success, False on failure
+ */
+ public function set_metadata($entries)
+ {
+ return $this->imap->set_metadata($this->name, $entries);
+ }
+
+
+ /**
+ *
+ */
+ public function get_folder_info()
+ {
+ if (!isset($this->info))
+ $this->info = $this->imap->folder_info($this->name);
+
+ return $this->info;
+ }
+
+ /**
+ * Make IMAP folder data available for this folder
+ */
+ public function get_imap_data()
+ {
+ if (!isset($this->idata))
+ $this->idata = $this->imap->folder_data($this->name);
+
+ return $this->idata;
+ }
+
+
+ /**
+ * Get IMAP ACL information for this folder
+ *
+ * @return string Permissions as string
+ */
+ public function get_myrights()
+ {
+ $rights = $this->info['rights'];
+
+ if (!is_array($rights))
+ $rights = $this->imap->my_rights($this->name);
+
+ return join('', (array)$rights);
+ }
+
+
+ /**
+ * Check activation status of this folder
+ *
+ * @return boolean True if enabled, false if not
+ */
+ public function is_active()
+ {
+ return kolab_storage::folder_is_active($this->name);
+ }
+
+ /**
+ * Change activation status of this folder
+ *
+ * @param boolean The desired subscription status: true = active, false = not active
+ *
+ * @return True on success, false on error
+ */
+ public function activate($active)
+ {
+ return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
+ }
+
+ /**
+ * Check subscription status of this folder
+ *
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed()
+ {
+ return kolab_storage::folder_is_subscribed($this->name);
+ }
+
+ /**
+ * Change subscription status of this folder
+ *
+ * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+ *
+ * @return True on success, false on error
+ */
+ public function subscribe($subscribed)
+ {
+ return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
+ }
+
+}
+
diff --git a/plugins/libkolab/lib/kolab_storage_folder_user.php b/plugins/libkolab/lib/kolab_storage_folder_user.php
new file mode 100644
index 0000000..70ded87
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_folder_user.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * Class that represents a (virtual) folder in the 'other' namespace
+ * implementing a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class kolab_storage_folder_user extends kolab_storage_folder_virtual
+{
+ protected static $ldapcache = array();
+
+ public $ldaprec;
+
+ /**
+ * Default constructor
+ */
+ public function __construct($name, $parent = '', $ldaprec = null)
+ {
+ parent::__construct($name, $name, 'other', $parent);
+
+ if (!empty($ldaprec)) {
+ self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
+ }
+ // use value cached in memory for repeated lookups
+ else if (array_key_exists($name, self::$ldapcache)) {
+ $this->ldaprec = self::$ldapcache[$name];
+ }
+ // lookup user in LDAP and set $this->ldaprec
+ else if ($ldap = kolab_storage::ldap()) {
+ // get domain from current user
+ list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
+ $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
+ if (!empty($this->ldaprec)) {
+ $this->ldaprec['kolabtargetfolder'] = $name;
+ }
+ self::$ldapcache[$name] = $this->ldaprec;
+ }
+ }
+
+ /**
+ * Getter for the top-end folder name to be displayed
+ *
+ * @return string Name of this folder
+ */
+ public function get_foldername()
+ {
+ return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
+ parent::get_foldername();
+ }
+
+ /**
+ * Returns the owner of the folder.
+ *
+ * @return string The owner of this folder.
+ */
+ public function get_owner()
+ {
+ return $this->ldaprec['mail'];
+ }
+
+ /**
+ * Check subscription status of this folder
+ *
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed()
+ {
+ return kolab_storage::folder_is_subscribed($this->name, true);
+ }
+
+ /**
+ * Change subscription status of this folder
+ *
+ * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+ *
+ * @return True on success, false on error
+ */
+ public function subscribe($subscribed)
+ {
+ return $subscribed ?
+ kolab_storage::folder_subscribe($this->name, true) :
+ kolab_storage::folder_unsubscribe($this->name, true);
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_folder_virtual.php b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
new file mode 100644
index 0000000..8b85ad5
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class kolab_storage_folder_virtual extends kolab_storage_folder_api
+{
+ public $virtual = true;
+
+ protected $displayname;
+
+ public function __construct($name, $dispname, $ns, $parent = '')
+ {
+ parent::__construct($name);
+
+ $this->namespace = $ns;
+ $this->parent = $parent;
+ $this->displayname = $dispname;
+ }
+
+ /**
+ * Get the display name value of this folder
+ *
+ * @return string Folder name
+ */
+ public function get_name()
+ {
+ // this is already kolab_storage::object_name() result
+ return $this->displayname;
+ }
+
+ /**
+ * Get the color value stored in metadata
+ *
+ * @param string Default color value to return if not set
+ * @return mixed Color value from IMAP metadata or $default is not set
+ */
+ public function get_color($default = null)
+ {
+ return $default;
+ }
+}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_user_folder.php b/plugins/libkolab/lib/kolab_storage_user_folder.php
deleted file mode 100644
index 55e38a0..0000000
--- a/plugins/libkolab/lib/kolab_storage_user_folder.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-/**
- * Class that represents a (virtual) folder in the 'other' namespace
- * implementing a subset of the kolab_storage_folder API.
- *
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-class kolab_storage_user_folder extends kolab_storage_virtual_folder
-{
- protected static $ldapcache = array();
-
- public $ldaprec;
-
- /**
- * Default constructor
- */
- public function __construct($name, $parent = '', $ldaprec = null)
- {
- parent::__construct($name, $name, 'other', $parent);
-
- if (!empty($ldaprec)) {
- self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
- }
- // use value cached in memory for repeated lookups
- else if (array_key_exists($name, self::$ldapcache)) {
- $this->ldaprec = self::$ldapcache[$name];
- }
- // lookup user in LDAP and set $this->ldaprec
- else if ($ldap = kolab_storage::ldap()) {
- // get domain from current user
- list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
- $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
- if (!empty($this->ldaprec)) {
- $this->ldaprec['kolabtargetfolder'] = $name;
- }
- self::$ldapcache[$name] = $this->ldaprec;
- }
- }
-
- /**
- * Getter for the top-end folder name to be displayed
- *
- * @return string Name of this folder
- */
- public function get_foldername()
- {
- return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
- parent::get_foldername();
- }
-
- /**
- * Returns the owner of the folder.
- *
- * @return string The owner of this folder.
- */
- public function get_owner()
- {
- return $this->ldaprec['mail'];
- }
-
- /**
- * Check activation status of this folder
- *
- * @return boolean True if enabled, false if not
- */
- public function is_active()
- {
- return kolab_storage::folder_is_active($this->name);
- }
-
- /**
- * Change activation status of this folder
- *
- * @param boolean The desired subscription status: true = active, false = not active
- *
- * @return True on success, false on error
- */
- public function activate($active)
- {
- return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
- }
-
- /**
- * Check subscription status of this folder
- *
- * @return boolean True if subscribed, false if not
- */
- public function is_subscribed()
- {
- return kolab_storage::folder_is_subscribed($this->name, true);
- }
-
- /**
- * Change subscription status of this folder
- *
- * @param boolean The desired subscription status: true = subscribed, false = not subscribed
- *
- * @return True on success, false on error
- */
- public function subscribe($subscribed)
- {
- return $subscribed ?
- kolab_storage::folder_subscribe($this->name, true) :
- kolab_storage::folder_unsubscribe($this->name, true);
- }
-
-}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_virtual_folder.php b/plugins/libkolab/lib/kolab_storage_virtual_folder.php
deleted file mode 100644
index 61d0fe0..0000000
--- a/plugins/libkolab/lib/kolab_storage_virtual_folder.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-/**
- * Helper class that represents a virtual IMAP folder
- * with a subset of the kolab_storage_folder API.
- *
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-class kolab_storage_virtual_folder
-{
- public $id;
- public $name;
- public $namespace;
- public $parent = '';
- public $children = array();
- public $virtual = true;
-
- protected $displayname;
-
- public function __construct($name, $dispname, $ns, $parent = '')
- {
- $this->id = kolab_storage::folder_id($name);
- $this->name = $name;
- $this->namespace = $ns;
- $this->parent = $parent;
- $this->displayname = $dispname;
- }
-
- /**
- * Getter for the name of the namespace to which the IMAP folder belongs
- *
- * @return string Name of the namespace (personal, other, shared)
- */
- public function get_namespace()
- {
- return $this->namespace;
- }
-
- /**
- * Get the display name value of this folder
- *
- * @return string Folder name
- */
- public function get_name()
- {
- // this is already kolab_storage::object_name() result
- return $this->displayname;
- }
-
- /**
- * Getter for the top-end folder name (not the entire path)
- *
- * @return string Name of this folder
- */
- public function get_foldername()
- {
- $parts = explode('/', $this->name);
- return rcube_charset::convert(end($parts), 'UTF7-IMAP');
- }
-
- /**
- * Get the color value stored in metadata
- *
- * @param string Default color value to return if not set
- * @return mixed Color value from IMAP metadata or $default is not set
- */
- public function get_color($default = null)
- {
- return $default;
- }
-}
\ No newline at end of file
commit 715b2b790a39ab293ba58d8b214cf4fb7faed661
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu May 15 11:57:54 2014 +0200
Fix listing of other user's calendars and sub-folders
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index a1a60b1..38a32a8 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2705,7 +2705,7 @@ function rcube_calendar_ui(settings)
// insert to #calendar-select options if writeable
select = $('#edit-calendar');
if (fc && !cal.readonly && select.length && !select.find('option[value="'+id+'"]').length) {
- $('<option>').attr('value', id).text(Q(cal.name)).appendTo(select);
+ $('<option>').attr('value', id).html(cal.name).appendTo(select);
}
}
@@ -2751,7 +2751,6 @@ function rcube_calendar_ui(settings)
calendars_list.addEventListener('insert-item', function(p) {
var cal = p.data;
if (cal && cal.id) {
- cal.active = true;
add_calendar_source(cal);
// add css classes related to this calendar to document
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 97f6a96..374ab5d 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -33,6 +33,8 @@ class kolab_calendar
public $alarms = false;
public $categories = array();
public $storage;
+
+ public $type = 'event';
public $name;
protected $cal;
@@ -198,6 +200,25 @@ class kolab_calendar
/**
+ * Update properties of this calendar folder
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function update(&$prop)
+ {
+ $prop['oldname'] = $this->get_realname();
+ $newfolder = kolab_storage::folder_update($prop);
+
+ if ($newfolder === false) {
+ $this->cal->last_error = $this->cal->gettext(kolab_storage::$last_error);
+ return false;
+ }
+
+ // create ID
+ return kolab_storage::folder_id($newfolder);
+ }
+
+ /**
* Getter for a single event object
*/
public function get_event($id)
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 12e4258..13c460e 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -129,7 +129,13 @@ class kolab_driver extends calendar_driver
$listname = $cal->get_foldername();
$imap_path = explode('/', $cal->name);
$topname = array_pop($imap_path);
- $parent_id = kolab_storage::folder_id(join('/', $imap_path), true);
+ $parent_id = kolab_storage::folder_id(join('/', $imap_path));
+
+ // turn a kolab_storage_folder object into a kolab_calendar
+ if ($cal instanceof kolab_storage_folder) {
+ $cal = new kolab_calendar($cal->name, $this->cal);
+ $this->calendars[$cal->id] = $cal;
+ }
// special handling for user or virtual folders
if ($cal instanceof kolab_storage_user_folder) {
@@ -141,7 +147,7 @@ class kolab_driver extends calendar_driver
'color' => $cal->get_color(),
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
- 'virtual' => false,
+ 'virtual' => false,
'readonly' => true,
'class_name' => 'user',
);
@@ -306,16 +312,7 @@ class kolab_driver extends calendar_driver
public function edit_calendar($prop)
{
if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
- $prop['oldname'] = $cal->get_realname();
- $newfolder = kolab_storage::folder_update($prop);
-
- if ($newfolder === false) {
- $this->last_error = $this->cal->gettext(kolab_storage::$last_error);
- return false;
- }
-
- // create ID
- $id = kolab_storage::folder_id($newfolder);
+ $id = $cal->update($prop);
}
else {
$id = $prop['id'];
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index a5795a4..b23d134 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -130,6 +130,20 @@ class kolab_user_calendar extends kolab_calendar
return false;
}
+
+ /**
+ * Update properties of this calendar folder
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function update(&$prop)
+ {
+ // don't change anything.
+ // let kolab_driver save props in local prefs
+ return $prop['id'];
+ }
+
+
/**
* Getter for a single event object
*/
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index a3ed443..4584f19 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -229,7 +229,7 @@ class calendar_ui
if ($attrib['activeonly'] && !$prop['active'])
continue;
- $html .= html::tag('li', array('id' => 'rcmlical' . rcube_utils::html_identifier($id)),
+ $html .= html::tag('li', array('id' => 'rcmlical' . $id),
$content = $this->calendar_list_item($id, $prop, $jsenv)
);
}
@@ -243,7 +243,7 @@ class calendar_ui
/**
* Return html for a structured list <ul> for the folder tree
*/
- public function list_tree_html(&$node, &$data, &$jsenv, $attrib)
+ public function list_tree_html($node, $data, &$jsenv, $attrib)
{
$out = '';
foreach ($node->children as $folder) {
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index b6e7ff2..92b4fb5 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -86,7 +86,7 @@ $labels['nmonthsback'] = '$nr months back';
$labels['showurl'] = 'Show calendar URL';
$labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
-$labels['calsearchresults'] = 'Additional Results';
+$labels['calsearchresults'] = 'Available Calendars';
// agenda view
$labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index a44899b..71dd106 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -297,6 +297,10 @@ pre {
padding: 2px 8px 2px 8px;
}
+#calendars .searchresults .listing li {
+ background-color: #c7e3ef;
+}
+
#calfeedurl,
#caldavurl {
width: 98%;
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index e2119a8..3c35846 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -62,39 +62,12 @@ function kolab_folderlist(node, p)
return;
var li = $(this).closest('li'),
- id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+ id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '')
node = search_results_widget.get_node(id),
- prop = search_results[id],
- parent_id = prop.parent || null,
- has_children = node.children && node.children.length,
- dom_node = has_children ? li.children().first().clone(true, true) : li.children().first();
-
- // find parent node and insert at the right place
- if (parent_id && $('#' + p.id_prefix + parent_id, me.container).length) {
- prop.listname = prop.editname;
- dom_node.children('span,a').first().html(Q(prop.listname));
- }
-
- // TODO: copy parent tree too
-
- // replace virtual node with a real one
- if (me.get_node(id)) {
- $(me.get_item(id, true)).children().first()
- .replaceWith(dom_node)
- .removeClass('virtual');
- }
- else {
- // move this result item to the main list widget
- me.insert({
- id: id,
- classes: [],
- virtual: prop.virtual,
- html: dom_node,
- }, parent_id, parent_id ? true : false);
- }
+ has_children = node.children && node.children.length;
- delete prop.html;
- me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+ // copy item to the main list
+ add_result2list(id, li, true);
if (has_children) {
li.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
@@ -127,6 +100,49 @@ function kolab_folderlist(node, p)
}
}
+ // helper method to (recursively) add a search result item to the main list widget
+ function add_result2list(id, li, active)
+ {
+ var node = search_results_widget.get_node(id),
+ prop = search_results[id],
+ parent_id = prop.parent || null,
+ has_children = node.children && node.children.length,
+ dom_node = has_children ? li.children().first().clone(true, true) : li.children().first();
+
+ // find parent node and insert at the right place
+ if (parent_id && me.get_node(parent_id)) {
+ dom_node.children('span,a').first().html(Q(prop.editname));
+ }
+ else if (parent_id && search_results[parent_id]) {
+ // copy parent tree from search results
+ add_result2list(parent_id, $(search_results_widget.get_item(parent_id)), false);
+ }
+ else if (parent_id) {
+ // use full name for list display
+ dom_node.children('span,a').first().html(Q(prop.name));
+ }
+
+ // replace virtual node with a real one
+ if (me.get_node(id)) {
+ $(me.get_item(id, true)).children().first()
+ .replaceWith(dom_node)
+ .removeClass('virtual');
+ }
+ else {
+ // move this result item to the main list widget
+ me.insert({
+ id: id,
+ classes: [],
+ virtual: prop.virtual,
+ html: dom_node,
+ }, parent_id, parent_id ? true : false);
+ }
+
+ delete prop.html;
+ prop.active = active;
+ me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+ }
+
// do some magic when search is performed on the widget
this.addEventListener('search', function(search) {
// hide search results
commit df08826c0346683ee3dc9892fcd67b68e5dcf7c1
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu May 15 11:57:12 2014 +0200
Improve listing of user folders: also list them if one has access to a child folder
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 872ce29..17c4912 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -299,6 +299,24 @@ class kolab_storage
/**
+ * Return the (first) path of the requested IMAP namespace
+ *
+ * @param string Namespace name (personal, shared, other)
+ * @return string IMAP root path for that namespace
+ */
+ public static function namespace_root($name)
+ {
+ foreach ((array)self::$imap->get_namespace($name) as $paths) {
+ if (strlen($paths[0]) > 1) {
+ return $paths[0];
+ }
+ }
+
+ return '/';
+ }
+
+
+ /**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
@@ -868,14 +886,10 @@ class kolab_storage
{
$_folders = array();
$delim = self::$imap->get_hierarchy_delimiter();
- $other_ns = self::$imap->get_namespace('other');
+ $other_ns = rtrim(self::namespace_root('other'), $delim);
$tree = new kolab_storage_virtual_folder('', '<root>', ''); // create tree root
$refs = array('' => $tree);
- if (is_array($other_ns)) {
- $other_ns = rtrim($other_ns[0][0], '/');
- }
-
foreach ($folders as $idx => $folder) {
$path = explode($delim, $folder->name);
array_pop($path);
@@ -894,7 +908,11 @@ class kolab_storage
array_pop($path);
$parent_parent = join($delim, $path);
if (!$refs[$parent]) {
- if ($parent_parent == $other_ns) {
+ if ($folder->type && self::folder_type($parent) == $folder->type) {
+ $refs[$parent] = new kolab_storage_folder($parent, $folder->type);
+ $refs[$parent]->parent = $parent_parent;
+ }
+ else if ($parent_parent == $other_ns) {
$refs[$parent] = new kolab_storage_user_folder($parent, $parent_parent);
}
else {
@@ -1314,12 +1332,11 @@ class kolab_storage
$results = self::$ldap->search(array('cn','mail','alias'), $query, $mode, $required, $limit);
// resolve to IMAP folder name
- $other_ns = self::$imap->get_namespace('other');
+ $root = self::namespace_root('other');
$user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
- array_walk($results, function(&$user, $dn) use ($other_ns, $user_attrib) {
+ array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
list($localpart, $domain) = explode('@', $user[$user_attrib]);
- $root = $other_ns[0][0];
$user['kolabtargetfolder'] = $root . $localpart;
});
@@ -1346,11 +1363,7 @@ class kolab_storage
if (!empty($user[$user_attrib])) {
list($mbox) = explode('@', $user[$user_attrib]);
- $other_ns = self::$imap->get_namespace('other');
- if (is_array($other_ns)) {
- $other_ns = $other_ns[0][0];
- }
-
+ $other_ns = self::namespace_root('other');
$folders = self::list_folders($other_ns . $mbox, '*', $type, $subscribed, $folderdata);
}
@@ -1363,7 +1376,7 @@ class kolab_storage
*
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
- * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
+ * @return array List of kolab_storage_user_folder objects
*/
public static function get_user_folders($subscribed)
{
@@ -1371,19 +1384,15 @@ class kolab_storage
if (self::setup()) {
$delimiter = self::$imap->get_hierarchy_delimiter();
- $other_ns = self::$imap->get_namespace('other');
- if (is_array($other_ns)) {
- $other_ns = rtrim($other_ns[0][0], $delimiter);
- $other_depth = count(explode($delimiter, $other_ns));
- }
+ $other_ns = rtrim(self::namespace_root('other'), $delimiter);
+ $path_len = count(explode($delimiter, $other_ns));
foreach ((array)self::list_folders($other_ns, '*', '', $subscribed) as $foldername) {
+ // truncate folder path to top-level folders of the 'other' namespace
$path = explode($delimiter, $foldername);
- $depth = count($path) - $other_depth;
- array_pop($path);
+ $foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
- // only list top-level folders of the 'other' namespace
- if ($depth == 1) {
+ if (!$folders[$foldername]) {
$folders[$foldername] = new kolab_storage_user_folder($foldername, $other_ns);
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 7db493d..39d1964 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -25,6 +25,12 @@
class kolab_storage_folder
{
/**
+ * Folder identifier
+ * @var string
+ */
+ public $id;
+
+ /**
* The folder name.
* @var string
*/
@@ -89,6 +95,7 @@ class kolab_storage_folder
$this->default = $suffix == 'default';
$this->name = $name;
$this->resource_uri = null;
+ $this->id = kolab_storage::folder_id($name);
// get a new cache instance of folder type changed
if (!$this->cache || $type != $oldtype)
commit 701c3391fe149a4e82535ae7d48fedd30ce70a77
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed May 14 20:37:06 2014 +0200
Search in LDAP and collect accessible folders (#3041)
- Add LDAP user search capabilities to kolab_storage class (using kolab_auth plugin classes)
- Introduce virtual 'user' folder objects and add methods to list them
- New 'user calendar' class in calendar (kolab driver)
- Render folder search results as hierarchical list
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 52baf11..a1a60b1 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2739,7 +2739,9 @@ function rcube_calendar_ui(settings)
selectable: true,
save_state: true,
searchbox: '#calendarlistsearch',
- search_action: 'calendar/calendar'
+ search_action: 'calendar/calendar',
+ search_sources: [ 'folders', 'users' ],
+ search_title: rcmail.gettext('calsearchresults','calendar')
});
calendars_list.addEventListener('select', function(node) {
me.select_calendar(node.id);
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 5feed23..97f6a96 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -7,7 +7,7 @@
* @author Thomas Bruederli <bruederli at kolabsys.com>
* @author Aleksander Machniak <machniak at kolabsys.com>
*
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -35,11 +35,30 @@ class kolab_calendar
public $storage;
public $name;
- private $cal;
- private $events = array();
- private $imap_folder = 'INBOX/Calendar';
- private $search_fields = array('title', 'description', 'location', 'attendees');
+ protected $cal;
+ protected $events = array();
+ protected $imap_folder = 'INBOX/Calendar';
+ protected $search_fields = array('title', 'description', 'location', 'attendees');
+ /**
+ * Factory method to instantiate a kolab_calendar object
+ *
+ * @param string Calendar ID (encoded IMAP folder name)
+ * @param object calendar plugin object
+ * @return object kolab_calendar instance
+ */
+ public static function factory($id, $calendar)
+ {
+ $imap = $calendar->rc->get_storage();
+ $imap_folder = kolab_storage::id_decode($id);
+ $info = $imap->folder_info($imap_folder, true);
+ if (empty($info) || $info['noselect'] || kolab_storage::folder_type($imap_folder) != 'event') {
+ return new kolab_user_calendar($imap_folder, $calendar);
+ }
+ else {
+ return new kolab_calendar($imap_folder, $calendar);
+ }
+ }
/**
* Default constructor
@@ -177,14 +196,6 @@ class kolab_calendar
return false;
}
- /**
- * Return the corresponding kolab_storage_folder instance
- */
- public function get_folder()
- {
- return $this->storage;
- }
-
/**
* Getter for a single event object
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 9c45eb6..12e4258 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -24,6 +24,7 @@
*/
require_once(dirname(__FILE__) . '/kolab_calendar.php');
+require_once(dirname(__FILE__) . '/kolab_user_calendar.php');
class kolab_driver extends calendar_driver
{
@@ -78,14 +79,20 @@ class kolab_driver extends calendar_driver
return $this->calendars;
// get all folders that have "event" type, sorted by namespace/name
- $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event'));
+ $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders(true));
$this->calendars = array();
foreach ($folders as $folder) {
- $calendar = new kolab_calendar($folder->name, $this->cal);
- $this->calendars[$calendar->id] = $calendar;
- if (!$calendar->readonly)
- $this->has_writeable = true;
+ if ($folder instanceof kolab_storage_user_folder)
+ $calendar = new kolab_user_calendar($folder->name, $this->cal);
+ else
+ $calendar = new kolab_calendar($folder->name, $this->cal);
+
+ if ($calendar->ready) {
+ $this->calendars[$calendar->id] = $calendar;
+ if (!$calendar->readonly)
+ $this->has_writeable = true;
+ }
}
return $this->calendars;
@@ -114,18 +121,32 @@ class kolab_driver extends calendar_driver
$calendars = $names = array();
// include virtual folders for a full folder tree
- if (!$active && !$personal && !$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
+ if (!is_null($tree))
$folders = kolab_storage::folder_hierarchy($folders, $tree);
foreach ($folders as $id => $cal) {
$fullname = $cal->get_name();
- $listname = kolab_storage::folder_displayname($fullname, $names);
+ $listname = $cal->get_foldername();
$imap_path = explode('/', $cal->name);
$topname = array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join('/', $imap_path), true);
- // special handling for virtual folders
- if ($cal->virtual) {
+ // special handling for user or virtual folders
+ if ($cal instanceof kolab_storage_user_folder) {
+ $calendars[$cal->id] = array(
+ 'id' => $cal->id,
+ 'name' => kolab_storage::object_name($fullname),
+ 'listname' => $listname,
+ 'editname' => $cal->get_foldername(),
+ 'color' => $cal->get_color(),
+ 'active' => $cal->is_active(),
+ 'owner' => $cal->get_owner(),
+ 'virtual' => false,
+ 'readonly' => true,
+ 'class_name' => 'user',
+ );
+ }
+ else if ($cal->virtual) {
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $fullname,
@@ -230,8 +251,8 @@ class kolab_driver extends calendar_driver
{
// create calendar object if necesary
if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
- $foldername = kolab_storage::id_decode($id);
- $calendar = new kolab_calendar($foldername, $this->cal);
+ $calendar = kolab_calendar::factory($id, $this->cal);
+ console($id, $calendar->id, $calendar->ready);
if ($calendar->ready)
$this->calendars[$calendar->id] = $calendar;
}
@@ -389,8 +410,20 @@ class kolab_driver extends calendar_driver
$this->calendars[$calendar->id] = $calendar;
}
}
+ // find other user's virtual calendars
else if ($source == 'users') {
- // TODO: implement this
+ foreach (kolab_storage::search_users($query, 0) as $user) {
+ $calendar = new kolab_user_calendar($user, $this->cal);
+ $this->calendars[$calendar->id] = $calendar;
+
+ // search for calendar folders shared by this user
+ foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
+ if (1 || !kolab_storage::folder_is_subscribed($foldername, true)) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ $this->calendars[$cal->id] = $cal;
+ }
+ }
+ }
}
// don't list the birthday calendar
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
new file mode 100644
index 0000000..a5795a4
--- /dev/null
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -0,0 +1,219 @@
+<?php
+
+/**
+ * Kolab calendar storage class simulating a virtual user calendar
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_user_calendar extends kolab_calendar
+{
+ public $id = 'unknown';
+ public $ready = false;
+ public $readonly = true;
+ public $attachments = false;
+ public $name;
+
+ protected $userdata = array();
+
+
+ /**
+ * Default constructor
+ */
+ public function __construct($user_or_folder, $calendar)
+ {
+ $this->cal = $calendar;
+
+ // full user record is provided
+ if (is_array($user_or_folder)) {
+ $this->userdata = $user_or_folder;
+ $this->storage = new kolab_storage_user_folder($this->userdata['kolabtargetfolder'], '', $this->userdata);
+ }
+ else { // get user record from LDAP
+ $this->storage = new kolab_storage_user_folder($user_or_folder);
+ $this->userdata = $this->storage->ldaprec;
+ }
+
+ $this->ready = !empty($this->userdata['kolabtargetfolder']);
+
+ if ($this->ready) {
+ // ID is derrived from the user's kolabtargetfolder attribute
+ $this->id = kolab_storage::folder_id($this->userdata['kolabtargetfolder'], true);
+ $this->imap_folder = $this->userdata['kolabtargetfolder'];
+ $this->name = $this->storage->get_name();
+
+ // user-specific alarms settings win
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+ if (isset($prefs[$this->id]['showalarms']))
+ $this->alarms = $prefs[$this->id]['showalarms'];
+ }
+ }
+
+
+ /**
+ * Getter for a nice and human readable name for this calendar
+ *
+ * @return string Name of this calendar
+ */
+ public function get_name()
+ {
+ return $this->userdata['name'] ?: $this->userdata['mail'];
+ }
+
+
+ /**
+ * Getter for the IMAP folder owner
+ *
+ * @return string Name of the folder owner
+ */
+ public function get_owner()
+ {
+ return $this->userdata['mail'];
+ }
+
+
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ return 'user';
+ }
+
+
+ /**
+ * Getter for the top-end calendar folder name (not the entire path)
+ *
+ * @return string Name of this calendar
+ */
+ public function get_foldername()
+ {
+ return $this->get_name();
+ }
+
+ /**
+ * Return color to display this calendar
+ */
+ public function get_color()
+ {
+ // calendar color is stored in local user prefs
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+
+ if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
+ return $prefs[$this->id]['color'];
+
+ return 'cc0000';
+ }
+
+ /**
+ * Compose an URL for CalDAV access to this calendar (if configured)
+ */
+ public function get_caldav_url()
+ {
+ return false;
+ }
+
+ /**
+ * Getter for a single event object
+ */
+ public function get_event($id)
+ {
+ // TODO: implement this
+ return $this->events[$id];
+ }
+
+
+ /**
+ * @param integer Event's new start (unix timestamp)
+ * @param integer Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param boolean Include virtual events (optional)
+ * @param array Additional parameters to query storage
+ * @return array A list of event records
+ */
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
+ {
+ // TODO: implement this
+ console('kolab_user_calendar::list_events()');
+ return array();
+ }
+
+
+ /**
+ * Create a new event record
+ *
+ * @see calendar_driver::new_event()
+ *
+ * @return mixed The created record ID on success, False on error
+ */
+ public function insert_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Update a specific event record
+ *
+ * @see calendar_driver::new_event()
+ * @return boolean True on success, False on error
+ */
+
+ public function update_event($event, $exception_id = null)
+ {
+ return false;
+ }
+
+ /**
+ * Delete an event record
+ *
+ * @see calendar_driver::remove_event()
+ * @return boolean True on success, False on error
+ */
+ public function delete_event($event, $force = true)
+ {
+ return false;
+ }
+
+ /**
+ * Restore deleted event record
+ *
+ * @see calendar_driver::undelete_event()
+ * @return boolean True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ return false;
+ }
+
+
+ /**
+ * Convert from Kolab_Format to internal representation
+ */
+ private function _to_rcube_event($record)
+ {
+ $record['id'] = $record['uid'];
+ $record['calendar'] = $this->id;
+
+ // TODO: implement this
+
+ return $record;
+ }
+
+}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 0f5091a..a3ed443 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -205,6 +205,7 @@ class calendar_ui
{
$html = '';
$jsenv = array();
+ $tree = true;
$calendars = $this->cal->driver->list_calendars(false, false, $tree);
// walk folder tree
@@ -273,19 +274,22 @@ class calendar_ui
*/
public function calendar_list_item($id, $prop, &$jsenv)
{
- unset($prop['user_id']);
- $prop['alarms'] = $this->cal->driver->alarms;
- $prop['attendees'] = $this->cal->driver->attendees;
- $prop['freebusy'] = $this->cal->driver->freebusy;
- $prop['attachments'] = $this->cal->driver->attachments;
- $prop['undelete'] = $this->cal->driver->undelete;
- $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
+ // enrich calendar properties with settings from the driver
+ if (!$prop['virtual']) {
+ unset($prop['user_id']);
+ $prop['alarms'] = $this->cal->driver->alarms;
+ $prop['attendees'] = $this->cal->driver->attendees;
+ $prop['freebusy'] = $this->cal->driver->freebusy;
+ $prop['attachments'] = $this->cal->driver->attachments;
+ $prop['undelete'] = $this->cal->driver->undelete;
+ $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
- if (!$prop['virtual'])
$jsenv[$id] = $prop;
+ }
$class = 'calendar cal-' . asciiwords($id, true);
- $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
+ $title = $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+ html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
$is_collapsed = false; // TODO: determine this somehow?
if ($prop['virtual'])
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 01a4c3d..a44899b 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -269,12 +269,12 @@ pre {
#calendars .treelist div.readonly span.calname {
background-position: right -20px;
}
-/*
-#calendars .treelist div.other span.calname {
+
+#calendars .treelist div.user span.calname {
background-position: right -38px;
}
-
-#calendars .treelist div.other.readonly span.calname {
+/*
+#calendars .treelist div.user.readonly span.calname {
background-position: right -56px;
}
diff --git a/plugins/kolab_auth/kolab_auth_ldap.php b/plugins/kolab_auth/kolab_auth_ldap.php
index d529b73..7044ebf 100644
--- a/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/plugins/kolab_auth/kolab_auth_ldap.php
@@ -213,7 +213,6 @@ class kolab_auth_ldap extends rcube_ldap_generic
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
- * @param boolean $select True if results are requested, False if count only
* @param array $required List of fields that cannot be empty
* @param int $limit Number of records
*
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index eed8b50..e2119a8 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -46,7 +46,7 @@ function kolab_folderlist(node, p)
// create treelist widget to present the search results
if (!search_results_widget) {
search_results_container = $('<div class="searchresults"></div>')
- .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
+ .html(p.search_title ? '<h2 class="boxtitle">' + p.search_title + '</h2>' : '')
.insertAfter(me.container);
search_results_widget = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
@@ -63,36 +63,64 @@ function kolab_folderlist(node, p)
var li = $(this).closest('li'),
id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+ node = search_results_widget.get_node(id),
prop = search_results[id],
- parent_id = prop.parent || null;
+ parent_id = prop.parent || null,
+ has_children = node.children && node.children.length,
+ dom_node = has_children ? li.children().first().clone(true, true) : li.children().first();
// find parent node and insert at the right place
if (parent_id && $('#' + p.id_prefix + parent_id, me.container).length) {
prop.listname = prop.editname;
- li.children().first().children('span,a').first().html(Q(prop.listname));
+ dom_node.children('span,a').first().html(Q(prop.listname));
}
- // move this result item to the main list widget
- me.insert({
- id: id,
- classes: [],
- html: li.children().first()
- }, parent_id, parent_id ? true : false);
+ // TODO: copy parent tree too
+
+ // replace virtual node with a real one
+ if (me.get_node(id)) {
+ $(me.get_item(id, true)).children().first()
+ .replaceWith(dom_node)
+ .removeClass('virtual');
+ }
+ else {
+ // move this result item to the main list widget
+ me.insert({
+ id: id,
+ classes: [],
+ virtual: prop.virtual,
+ html: dom_node,
+ }, parent_id, parent_id ? true : false);
+ }
delete prop.html;
me.triggerEvent('insert-item', { id: id, data: prop, item: li });
- li.remove();
+
+ if (has_children) {
+ li.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+ }
+ else {
+ li.remove();
+ }
});
}
// add results to list
- for (var prop, i=0; i < results.length; i++) {
+ for (var prop, item, i=0; i < results.length; i++) {
prop = results[i];
+ item = $(prop.html);
search_results[prop.id] = prop;
- $('<li>')
- .attr('id', p.id_prefix + prop.id)
- .html(prop.html)
- .appendTo(search_results_widget.container);
+ search_results_widget.insert({
+ id: prop.id,
+ classes: prop.class_name ? String(prop.class_name).split(' ') : [],
+ html: item,
+ collapsed: true
+ }, prop.parent);
+
+ // disable checkbox if item already exists in main list
+ if (me.get_node(prop.id) && !me.get_node(prop.id).virtual) {
+ item.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+ }
}
search_results_container.show();
@@ -110,7 +138,7 @@ function kolab_folderlist(node, p)
// send search request(s) to server
if (search.query && search.execute) {
- var sources = [ 'folders' /*, 'users'*/ ];
+ var sources = p.search_sources || [ 'folders' ];
var reqid = rcmail.multi_thread_http_request({
items: sources,
threads: rcmail.env.autocomplete_threads || 1,
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 970316e..872ce29 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -44,6 +44,7 @@ class kolab_storage
private static $states;
private static $config;
private static $imap;
+ private static $ldap;
// Default folder names
private static $default_folders = array(
@@ -101,6 +102,40 @@ class kolab_storage
return self::$ready;
}
+ /**
+ * Initializes LDAP object to resolve Kolab users
+ */
+ public static function ldap()
+ {
+ if (self::$ldap) {
+ return self::$ldap;
+ }
+
+ $rcmail = rcube::get_instance();
+ $config = $rcmail->config->get('kolab_users_directory', $rcmail->config->get('kolab_auth_addressbook'));
+
+ if (!is_array($config)) {
+ $ldap_config = (array)$rcmail->config->get('ldap_public');
+ $config = $ldap_config[$config];
+ }
+
+ if (empty($config)) {
+ return null;
+ }
+
+ // overwrite filter option
+ if ($filter = $rcmail->config->get('kolab_users_filter')) {
+ $rcmail->config->set('kolab_auth_filter', $filter);
+ }
+
+ // re-use the LDAP wrapper class from kolab_auth plugin
+ require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php';
+
+ self::$ldap = new kolab_auth_ldap($config);
+
+ return self::$ldap;
+ }
+
/**
* Get a list of storage folders for the given data type
@@ -490,7 +525,7 @@ class kolab_storage
$folder = substr($folder, $pos+1);
}
else {
- $prefix = $folder;
+ $prefix = '('.$folder.')';
$folder = '';
}
@@ -811,7 +846,7 @@ class kolab_storage
// $folders is a result of get_folders() we can assume folders were already sorted
foreach (array_keys($nsnames) as $ns) {
- // asort($nsnames[$ns], SORT_LOCALE_STRING);
+ asort($nsnames[$ns], SORT_LOCALE_STRING);
foreach (array_keys($nsnames[$ns]) as $utf7name) {
$out[] = $folders[$utf7name];
}
@@ -829,13 +864,18 @@ class kolab_storage
*
* @return array Flat folders list
*/
- public static function folder_hierarchy($folders, &$tree)
+ public static function folder_hierarchy($folders, &$tree = null)
{
$_folders = array();
- $delim = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
- $tree = new virtual_kolab_storage_folder('', '<root>', ''); // create tree root
+ $delim = self::$imap->get_hierarchy_delimiter();
+ $other_ns = self::$imap->get_namespace('other');
+ $tree = new kolab_storage_virtual_folder('', '<root>', ''); // create tree root
$refs = array('' => $tree);
+ if (is_array($other_ns)) {
+ $other_ns = rtrim($other_ns[0][0], '/');
+ }
+
foreach ($folders as $idx => $folder) {
$path = explode($delim, $folder->name);
array_pop($path);
@@ -852,9 +892,15 @@ class kolab_storage
while (count($path) >= $depth && ($parent = join($delim, $path))) {
array_pop($path);
- $name = kolab_storage::object_name($parent, $folder->get_namespace());
+ $parent_parent = join($delim, $path);
if (!$refs[$parent]) {
- $refs[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace(), join($delim, $path));
+ if ($parent_parent == $other_ns) {
+ $refs[$parent] = new kolab_storage_user_folder($parent, $parent_parent);
+ }
+ else {
+ $name = kolab_storage::object_name($parent, $folder->get_namespace());
+ $refs[$parent] = new kolab_storage_virtual_folder($parent, $name, $folder->get_namespace(), $parent_parent);
+ }
$parents[] = $refs[$parent];
}
}
@@ -1023,14 +1069,22 @@ class kolab_storage
* Change subscription status of this folder
*
* @param string $folder Folder name
+ * @param boolean $temp Only remove temporary subscription
*
* @return True on success, false on error
*/
- public static function folder_unsubscribe($folder)
+ public static function folder_unsubscribe($folder, $temp = false)
{
self::setup();
- if (self::$imap->unsubscribe($folder)) {
+ // temporary/session subscription
+ if ($temp) {
+ if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
+ unset($_SESSION['kolab_subscribed_folders'][$i]);
+ }
+ return true;
+ }
+ else if (self::$imap->unsubscribe($folder)) {
self::$subscriptions === null;
return true;
}
@@ -1078,10 +1132,8 @@ class kolab_storage
*/
public static function folder_deactivate($folder)
{
- // remove from temp subscriptions
- if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
- unset($_SESSION['kolab_subscribed_folders'][$i]);
- }
+ // remove from temp subscriptions, really?
+ self::folder_unsubscribe($folder, true);
return self::set_state($folder, false);
}
@@ -1241,57 +1293,117 @@ class kolab_storage
}
}
+
/**
- * Handler for user_delete plugin hooks
*
- * Remove all cache data from the local database related to the given user.
+ * @param mixed $query Search value (or array of field => value pairs)
+ * @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
+ * @param array $required List of fields that shall ot be empty
+ * @param int $limit Number of records
+ *
+ * @return array List or false on error
*/
- public static function delete_user_folders($args)
+ public static function search_users($query, $mode = 1, $required = array(), $limit = 0)
{
- $db = rcmail::get_instance()->get_dbh();
- $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
- $db->query("DELETE FROM " . $db->table_name('kolab_folders') . " WHERE resource LIKE ?", $prefix);
- }
+ // requires a working LDAP setup
+ if (!self::ldap()) {
+ return array();
+ }
-}
+ // FIXME: make search attributes configurable
+ $results = self::$ldap->search(array('cn','mail','alias'), $query, $mode, $required, $limit);
-/**
- * Helper class that represents a virtual IMAP folder
- * with a subset of the kolab_storage_folder API.
- */
-class virtual_kolab_storage_folder
-{
- public $id;
- public $name;
- public $namespace;
- public $parent = '';
- public $children = array();
- public $virtual = true;
- protected $displayname;
-
- public function __construct($name, $dispname, $ns, $parent = '')
- {
- $this->id = kolab_storage::folder_id($name);
- $this->name = $name;
- $this->namespace = $ns;
- $this->parent = $parent;
- $this->displayname = $dispname;
+ // resolve to IMAP folder name
+ $other_ns = self::$imap->get_namespace('other');
+ $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+
+ array_walk($results, function(&$user, $dn) use ($other_ns, $user_attrib) {
+ list($localpart, $domain) = explode('@', $user[$user_attrib]);
+ $root = $other_ns[0][0];
+ $user['kolabtargetfolder'] = $root . $localpart;
+ });
+
+ return $results;
}
- public function get_namespace()
+
+ /**
+ * Returns a list of IMAP folders shared by the given user
+ *
+ * @param array User entry from LDAP
+ * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
+ * @param boolean Return subscribed folders only (null to use configured subscription mode)
+ * @param array Will be filled with folder-types data
+ *
+ * @return array List of folders
+ */
+ public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
{
- return $this->namespace;
+ $folders = array();
+
+ // use localpart of user attribute as root for folder listing
+ $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+ if (!empty($user[$user_attrib])) {
+ list($mbox) = explode('@', $user[$user_attrib]);
+
+ $other_ns = self::$imap->get_namespace('other');
+ if (is_array($other_ns)) {
+ $other_ns = $other_ns[0][0];
+ }
+
+ $folders = self::list_folders($other_ns . $mbox, '*', $type, $subscribed, $folderdata);
+ }
+
+ return $folders;
}
- public function get_name()
+
+ /**
+ * Get a list of (virtual) top-level folders from the other users namespace
+ *
+ * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
+ *
+ * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
+ */
+ public static function get_user_folders($subscribed)
{
- // this is already kolab_storage::object_name() result
- return $this->displayname;
+ $folders = $folderdata = array();
+
+ if (self::setup()) {
+ $delimiter = self::$imap->get_hierarchy_delimiter();
+ $other_ns = self::$imap->get_namespace('other');
+ if (is_array($other_ns)) {
+ $other_ns = rtrim($other_ns[0][0], $delimiter);
+ $other_depth = count(explode($delimiter, $other_ns));
+ }
+
+ foreach ((array)self::list_folders($other_ns, '*', '', $subscribed) as $foldername) {
+ $path = explode($delimiter, $foldername);
+ $depth = count($path) - $other_depth;
+ array_pop($path);
+
+ // only list top-level folders of the 'other' namespace
+ if ($depth == 1) {
+ $folders[$foldername] = new kolab_storage_user_folder($foldername, $other_ns);
+ }
+ }
+ }
+
+ return $folders;
}
- public function get_foldername()
+
+ /**
+ * Handler for user_delete plugin hooks
+ *
+ * Remove all cache data from the local database related to the given user.
+ */
+ public static function delete_user_folders($args)
{
- $parts = explode('/', $this->name);
- return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+ $db = rcmail::get_instance()->get_dbh();
+ $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
+ $db->query("DELETE FROM " . $db->table_name('kolab_folders') . " WHERE resource LIKE ?", $prefix);
}
+
}
+
diff --git a/plugins/libkolab/lib/kolab_storage_user_folder.php b/plugins/libkolab/lib/kolab_storage_user_folder.php
new file mode 100644
index 0000000..55e38a0
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_user_folder.php
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * Class that represents a (virtual) folder in the 'other' namespace
+ * implementing a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class kolab_storage_user_folder extends kolab_storage_virtual_folder
+{
+ protected static $ldapcache = array();
+
+ public $ldaprec;
+
+ /**
+ * Default constructor
+ */
+ public function __construct($name, $parent = '', $ldaprec = null)
+ {
+ parent::__construct($name, $name, 'other', $parent);
+
+ if (!empty($ldaprec)) {
+ self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
+ }
+ // use value cached in memory for repeated lookups
+ else if (array_key_exists($name, self::$ldapcache)) {
+ $this->ldaprec = self::$ldapcache[$name];
+ }
+ // lookup user in LDAP and set $this->ldaprec
+ else if ($ldap = kolab_storage::ldap()) {
+ // get domain from current user
+ list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
+ $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
+ if (!empty($this->ldaprec)) {
+ $this->ldaprec['kolabtargetfolder'] = $name;
+ }
+ self::$ldapcache[$name] = $this->ldaprec;
+ }
+ }
+
+ /**
+ * Getter for the top-end folder name to be displayed
+ *
+ * @return string Name of this folder
+ */
+ public function get_foldername()
+ {
+ return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
+ parent::get_foldername();
+ }
+
+ /**
+ * Returns the owner of the folder.
+ *
+ * @return string The owner of this folder.
+ */
+ public function get_owner()
+ {
+ return $this->ldaprec['mail'];
+ }
+
+ /**
+ * Check activation status of this folder
+ *
+ * @return boolean True if enabled, false if not
+ */
+ public function is_active()
+ {
+ return kolab_storage::folder_is_active($this->name);
+ }
+
+ /**
+ * Change activation status of this folder
+ *
+ * @param boolean The desired subscription status: true = active, false = not active
+ *
+ * @return True on success, false on error
+ */
+ public function activate($active)
+ {
+ return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
+ }
+
+ /**
+ * Check subscription status of this folder
+ *
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed()
+ {
+ return kolab_storage::folder_is_subscribed($this->name, true);
+ }
+
+ /**
+ * Change subscription status of this folder
+ *
+ * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+ *
+ * @return True on success, false on error
+ */
+ public function subscribe($subscribed)
+ {
+ return $subscribed ?
+ kolab_storage::folder_subscribe($this->name, true) :
+ kolab_storage::folder_unsubscribe($this->name, true);
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_virtual_folder.php b/plugins/libkolab/lib/kolab_storage_virtual_folder.php
new file mode 100644
index 0000000..61d0fe0
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_virtual_folder.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class kolab_storage_virtual_folder
+{
+ public $id;
+ public $name;
+ public $namespace;
+ public $parent = '';
+ public $children = array();
+ public $virtual = true;
+
+ protected $displayname;
+
+ public function __construct($name, $dispname, $ns, $parent = '')
+ {
+ $this->id = kolab_storage::folder_id($name);
+ $this->name = $name;
+ $this->namespace = $ns;
+ $this->parent = $parent;
+ $this->displayname = $dispname;
+ }
+
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ return $this->namespace;
+ }
+
+ /**
+ * Get the display name value of this folder
+ *
+ * @return string Folder name
+ */
+ public function get_name()
+ {
+ // this is already kolab_storage::object_name() result
+ return $this->displayname;
+ }
+
+ /**
+ * Getter for the top-end folder name (not the entire path)
+ *
+ * @return string Name of this folder
+ */
+ public function get_foldername()
+ {
+ $parts = explode('/', $this->name);
+ return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+ }
+
+ /**
+ * Get the color value stored in metadata
+ *
+ * @param string Default color value to return if not set
+ * @return mixed Color value from IMAP metadata or $default is not set
+ */
+ public function get_color($default = null)
+ {
+ return $default;
+ }
+}
\ No newline at end of file
commit 8a47c676d52645f424a5234c9c6d2fdfa7536f49
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue May 13 19:14:08 2014 +0200
Move new calendar list widget and folder searching to libkolab for shared use
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 4db8593..52baf11 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1548,7 +1548,8 @@ function rcube_calendar_ui(settings)
id_prefix: 'rcres',
id_encode: rcmail.html_identifier_encode,
id_decode: rcmail.html_identifier_decode,
- selectable: true
+ selectable: true,
+ save_state: true
});
resources_treelist.addEventListener('select', function(node) {
if (resources_data[node.id]) {
@@ -2671,72 +2672,6 @@ function rcube_calendar_ui(settings)
this.selected_calendar = id;
};
- // render the results for calendar list search
- var calendar_search_results = function(results)
- {
- if (results.length) {
- // create treelist widget to present the search results
- if (!calenders_search_list) {
- calenders_search_container = $('<div class="searchresults"></div>')
- .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
- .insertAfter(rcmail.gui_objects.calendarslist);
-
- calenders_search_list = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
- id_prefix: 'rcmlical',
- selectable: false
- });
-
- // register click handler on search result's checkboxes to select the given calendar for listing
- calenders_search_list.container
- .appendTo(calenders_search_container)
- .on('click', 'input[type=checkbox]', function(e){
- var li = $(this).closest('li'),
- id = li.attr('id').replace(/^rcmlical/, ''),
- prop = search_calendars[id],
- parent_id = prop.parent || null;
-
- if (!this.checked)
- return;
-
- // find parent node and insert at the right place
- if (parent_id && $('#rcmlical'+parent_id, rcmail.gui_objects.calendarslist).length) {
- prop.listname = prop.editname;
- li.children().first().find('.calname').html(Q(prop.listname));
- }
-
- // move this calendar to the calendars_list widget
- calendars_list.insert({
- id: id,
- classes: [],
- html: li.children().first()
- }, parent_id, parent_id ? true : false);
-
- search_calendars[id].active = true;
- add_calendar_source(prop);
- li.remove();
-
- // add css classes related to this calendar to document
- if (cal.css) {
- $('<style type="text/css"></style>')
- .html(cal.css)
- .appendTo('head');
- }
- });
- }
-
- for (var cal, i=0; i < results.length; i++) {
- cal = results[i];
- search_calendars[cal.id] = cal;
- $('<li>')
- .attr('id', 'rcmlical' + cal.id)
- .html(cal.html)
- .appendTo(calenders_search_list.container);
- }
-
- calenders_search_container.show();
- }
- };
-
// register the given calendar to the current view
var add_calendar_source = function(cal)
{
@@ -2798,37 +2733,31 @@ function rcube_calendar_ui(settings)
}
// initialize treelist widget that controls the calendars list
- calendars_list = new rcube_treelist_widget(rcmail.gui_objects.calendarslist, {
+ var widget_class = window.kolab_folderlist || rcube_treelist_widget;
+ calendars_list = new widget_class(rcmail.gui_objects.calendarslist, {
id_prefix: 'rcmlical',
selectable: true,
- searchbox: '#calendarlistsearch'
+ save_state: true,
+ searchbox: '#calendarlistsearch',
+ search_action: 'calendar/calendar'
});
- calendars_list.addEventListener('select', function(node){
+ calendars_list.addEventListener('select', function(node) {
me.select_calendar(node.id);
rcmail.enable_command('calendar-edit', 'calendar-showurl', true);
rcmail.enable_command('calendar-remove', !me.calendars[node.id].readonly);
});
- calendars_list.addEventListener('search', function(search){
- // hide search results
- if (calenders_search_list) {
- calenders_search_container.hide();
- calenders_search_list.reset();
- }
- search_calendars = {};
-
- // send search request(s) to server
- if (search.query && search.execute) {
- var sources = [ 'folders' /*, 'users'*/ ];
- var reqid = rcmail.multi_thread_http_request({
- items: sources,
- threads: rcmail.env.autocomplete_threads || 1,
- action: 'calendar/calendar',
- postdata: { action:'search', q:search.query, source:'%s' },
- lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
- onresponse: calendar_search_results
- });
-
- listsearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
+ calendars_list.addEventListener('insert-item', function(p) {
+ var cal = p.data;
+ if (cal && cal.id) {
+ cal.active = true;
+ add_calendar_source(cal);
+
+ // add css classes related to this calendar to document
+ if (cal.css) {
+ $('<style type="text/css"></style>')
+ .html(cal.css)
+ .appendTo('head');
+ }
}
});
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 786556f..9c45eb6 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -381,23 +381,10 @@ class kolab_driver extends calendar_driver
return array();
$this->calendars = array();
- $imap = $this->rc->get_storage();
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
- $folders = array();
- foreach ((array)kolab_storage::list_folders('', '*', 'event', false, $folderdata) as $foldername) {
- // FIXME: only consider the last part of the folder path for searching?
- $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
- if (strpos($realname, $query) !== false &&
- !kolab_storage::folder_is_subscribed($foldername, true) &&
- $imap->folder_namespace($foldername) != 'other'
- ) {
- $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
- }
- }
-
- foreach ($folders as $folder) {
+ foreach ((array)kolab_storage::search_folders('event', $query, array('other')) as $folder) {
$calendar = new kolab_calendar($folder->name, $this->cal);
$this->calendars[$calendar->id] = $calendar;
}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 69ba9c2..0f5091a 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -117,6 +117,12 @@ class calendar_ui
$this->cal->include_script('calendar_ui.js');
$this->cal->include_script('lib/js/fullcalendar.js');
$this->rc->output->include_script('treelist.js');
+
+ // include kolab folderlist widget if available
+ if (is_readable($this->cal->home . '/lib/js/folderlist.js')) {
+ $this->cal->include_script('lib/js/folderlist.js');
+ }
+
jqueryui::miniColors();
}
@@ -292,9 +298,9 @@ class calendar_ui
$content = '';
if (!$attrib['activeonly'] || $prop['active']) {
$content = html::div($class,
+ html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
- html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')) .
- html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname'])
+ html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' '))
);
}
diff --git a/plugins/calendar/lib/js/folderlist.js b/plugins/calendar/lib/js/folderlist.js
new file mode 120000
index 0000000..c49706b
--- /dev/null
+++ b/plugins/calendar/lib/js/folderlist.js
@@ -0,0 +1 @@
+../../../libkolab/js/folderlist.js
\ No newline at end of file
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
new file mode 100644
index 0000000..eed8b50
--- /dev/null
+++ b/plugins/libkolab/js/folderlist.js
@@ -0,0 +1,130 @@
+/**
+ * Kolab groupware folders treelist widget
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this file.
+ */
+
+function kolab_folderlist(node, p)
+{
+ // extends treelist.js
+ rcube_treelist_widget.call(this, node, p);
+
+ // private vars
+ var me = this;
+ var search_results;
+ var search_results_widget;
+ var search_results_container;
+ var listsearch_request;
+
+ var Q = rcmail.quote_html;
+
+ // render the results for folderlist search
+ function render_search_results(results)
+ {
+ if (results.length) {
+ // create treelist widget to present the search results
+ if (!search_results_widget) {
+ search_results_container = $('<div class="searchresults"></div>')
+ .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
+ .insertAfter(me.container);
+
+ search_results_widget = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
+ id_prefix: p.id_prefix,
+ selectable: false
+ });
+
+ // register click handler on search result's checkboxes to select the given item for listing
+ search_results_widget.container
+ .appendTo(search_results_container)
+ .on('click', 'input[type=checkbox]', function(e) {
+ if (!this.checked)
+ return;
+
+ var li = $(this).closest('li'),
+ id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+ prop = search_results[id],
+ parent_id = prop.parent || null;
+
+ // find parent node and insert at the right place
+ if (parent_id && $('#' + p.id_prefix + parent_id, me.container).length) {
+ prop.listname = prop.editname;
+ li.children().first().children('span,a').first().html(Q(prop.listname));
+ }
+
+ // move this result item to the main list widget
+ me.insert({
+ id: id,
+ classes: [],
+ html: li.children().first()
+ }, parent_id, parent_id ? true : false);
+
+ delete prop.html;
+ me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+ li.remove();
+ });
+ }
+
+ // add results to list
+ for (var prop, i=0; i < results.length; i++) {
+ prop = results[i];
+ search_results[prop.id] = prop;
+ $('<li>')
+ .attr('id', p.id_prefix + prop.id)
+ .html(prop.html)
+ .appendTo(search_results_widget.container);
+ }
+
+ search_results_container.show();
+ }
+ }
+
+ // do some magic when search is performed on the widget
+ this.addEventListener('search', function(search) {
+ // hide search results
+ if (search_results_widget) {
+ search_results_container.hide();
+ search_results_widget.reset();
+ }
+ search_results = {};
+
+ // send search request(s) to server
+ if (search.query && search.execute) {
+ var sources = [ 'folders' /*, 'users'*/ ];
+ var reqid = rcmail.multi_thread_http_request({
+ items: sources,
+ threads: rcmail.env.autocomplete_threads || 1,
+ action: p.search_action || 'listsearch',
+ postdata: { action:'search', q:search.query, source:'%s' },
+ lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
+ onresponse: render_search_results
+ });
+
+ listsearch_request = { id:reqid, sources:sources.slice(), num:sources.length };
+ }
+ });
+
+}
+
+// link prototype from base class
+kolab_folderlist.prototype = rcube_treelist_widget.prototype;
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 55526fd..970316e 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -759,6 +759,39 @@ class kolab_storage
/**
+ * Search for shared or otherwise not listed groupware folders the user has access
+ *
+ * @param string Folder type of folders to search for
+ * @param string Search string
+ * @param array Namespace(s) to exclude results from
+ *
+ * @return array List of matching kolab_storage_folder objects
+ */
+ public static function search_folders($type, $query, $exclude_ns = array())
+ {
+ if (!self::setup()) {
+ return array();
+ }
+
+ $folders = array();
+
+ // find unsubscribed IMAP folders of the given type
+ foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
+ // FIXME: only consider the last part of the folder path for searching?
+ $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
+ if (strpos($realname, $query) !== false &&
+ !self::folder_is_subscribed($foldername, true) &&
+ !in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
+ ) {
+ $folders[] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ }
+ }
+
+ return $folders;
+ }
+
+
+ /**
* Sort the given list of kolab folders by namespace/name
*
* @param array List of kolab_storage_folder objects
commit 008c5db5d950f6dfee3458a20264f68a26e5cf6e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue May 13 17:09:53 2014 +0200
Implement searching for unsubscribed IMAP folders and temporary/session subscriptions
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 1d40efe..fc0069d 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -725,9 +725,29 @@ class calendar extends rcube_plugin
$this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
break;
case "subscribe":
- if (!$this->driver->subscribe_calendar($cal))
+ if (!$this->driver->subscribe_calendar($cal, intval(get_input_value('perm', RCUBE_INPUT_GPC))))
$this->rc->output->show_message($this->gettext('errorsaving'), 'error');
return;
+ case "search":
+ $results = array();
+ $color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
+ foreach ((array)$this->driver->search_calendars(get_input_value('q', RCUBE_INPUT_GPC), get_input_value('source', RCUBE_INPUT_GPC)) as $id => $prop) {
+ $editname = $prop['editname'];
+ unset($prop['editname']); // force full name to be displayed
+ $prop['active'] = false;
+
+ // let the UI generate HTML and CSS representation for this calendar
+ $html = $this->ui->calendar_list_item($id, $prop, $jsenv);
+ $cal = $jsenv[$id];
+ $cal['editname'] = $editname;
+ $cal['html'] = $html;
+ if (!empty($prop['color']))
+ $cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode);
+
+ $results[] = $cal;
+ }
+ $this->rc->output->command('multi_thread_http_response', $results, get_input_value('_reqid', RCUBE_INPUT_GPC));
+ return;
}
if ($success)
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 2a8a8d2..4db8593 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -39,6 +39,7 @@ function rcube_calendar_ui(settings)
this.selected_calendar = null;
this.search_request = null;
this.saving_lock;
+ this.calendars = {};
/*** private vars ***/
@@ -52,6 +53,9 @@ function rcube_calendar_ui(settings)
var event_defaults = { free_busy:'busy', alarms:'' };
var event_attendees = [];
var calendars_list;
+ var calenders_search_list;
+ var calenders_search_container;
+ var search_calendars = {};
var attendees_list;
var resources_list;
var resources_treelist;
@@ -2667,16 +2671,79 @@ function rcube_calendar_ui(settings)
this.selected_calendar = id;
};
+ // render the results for calendar list search
+ var calendar_search_results = function(results)
+ {
+ if (results.length) {
+ // create treelist widget to present the search results
+ if (!calenders_search_list) {
+ calenders_search_container = $('<div class="searchresults"></div>')
+ .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
+ .insertAfter(rcmail.gui_objects.calendarslist);
- /*** startup code ***/
+ calenders_search_list = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
+ id_prefix: 'rcmlical',
+ selectable: false
+ });
- // create list of event sources AKA calendars
- this.calendars = {};
- var id, li, cal, active, color, brightness, event_sources = [];
- for (id in rcmail.env.calendars) {
- cal = rcmail.env.calendars[id];
- this.calendars[id] = $.extend({
- url: "./?_task=calendar&_action=load_events&source="+escape(id),
+ // register click handler on search result's checkboxes to select the given calendar for listing
+ calenders_search_list.container
+ .appendTo(calenders_search_container)
+ .on('click', 'input[type=checkbox]', function(e){
+ var li = $(this).closest('li'),
+ id = li.attr('id').replace(/^rcmlical/, ''),
+ prop = search_calendars[id],
+ parent_id = prop.parent || null;
+
+ if (!this.checked)
+ return;
+
+ // find parent node and insert at the right place
+ if (parent_id && $('#rcmlical'+parent_id, rcmail.gui_objects.calendarslist).length) {
+ prop.listname = prop.editname;
+ li.children().first().find('.calname').html(Q(prop.listname));
+ }
+
+ // move this calendar to the calendars_list widget
+ calendars_list.insert({
+ id: id,
+ classes: [],
+ html: li.children().first()
+ }, parent_id, parent_id ? true : false);
+
+ search_calendars[id].active = true;
+ add_calendar_source(prop);
+ li.remove();
+
+ // add css classes related to this calendar to document
+ if (cal.css) {
+ $('<style type="text/css"></style>')
+ .html(cal.css)
+ .appendTo('head');
+ }
+ });
+ }
+
+ for (var cal, i=0; i < results.length; i++) {
+ cal = results[i];
+ search_calendars[cal.id] = cal;
+ $('<li>')
+ .attr('id', 'rcmlical' + cal.id)
+ .html(cal.html)
+ .appendTo(calenders_search_list.container);
+ }
+
+ calenders_search_container.show();
+ }
+ };
+
+ // register the given calendar to the current view
+ var add_calendar_source = function(cal)
+ {
+ var color, brightness, select, id = cal.id;
+
+ me.calendars[id] = $.extend({
+ url: rcmail.url('calendar/load_events', { source: id }),
editable: !cal.readonly,
className: 'fc-event-cal-'+id,
id: id
@@ -2689,18 +2756,40 @@ function rcube_calendar_ui(settings)
// http://javascriptrules.com/2009/08/05/css-color-brightness-contrast-using-javascript/
brightness = (parseInt(RegExp.$1, 16) * 299 + parseInt(RegExp.$2, 16) * 587 + parseInt(RegExp.$3, 16) * 114) / 1000;
if (brightness > 125)
- this.calendars[id].textColor = 'black';
+ me.calendars[id].textColor = 'black';
}
+
+ me.calendars[id].color = color;
}
- this.calendars[id].color = color;
+ if (fc && cal.active) {
+ fc.fullCalendar('addEventSource', me.calendars[id]);
+ rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:cal.active?1:0 } });
+ }
- if ((active = cal.active || false)) {
- event_sources.push(this.calendars[id]);
+ // insert to #calendar-select options if writeable
+ select = $('#edit-calendar');
+ if (fc && !cal.readonly && select.length && !select.find('option[value="'+id+'"]').length) {
+ $('<option>').attr('value', id).text(Q(cal.name)).appendTo(select);
}
+ }
+
+
+ /*** startup code ***/
+
+ // create list of event sources AKA calendars
+ var id, cal, active, event_sources = [];
+ for (id in rcmail.env.calendars) {
+ cal = rcmail.env.calendars[id];
+ active = cal.active || false;
+ add_calendar_source(cal);
// check active calendars
- $('#rcmlical'+id+' > .calendar input').data('id', id).get(0).checked = active;
+ $('#rcmlical'+id+' > .calendar input').get(0).checked = active;
+
+ if (active) {
+ event_sources.push(this.calendars[id]);
+ }
if (!cal.readonly && !this.selected_calendar) {
this.selected_calendar = id;
@@ -2720,12 +2809,32 @@ function rcube_calendar_ui(settings)
rcmail.enable_command('calendar-remove', !me.calendars[node.id].readonly);
});
calendars_list.addEventListener('search', function(search){
- console.log(search);
+ // hide search results
+ if (calenders_search_list) {
+ calenders_search_container.hide();
+ calenders_search_list.reset();
+ }
+ search_calendars = {};
+
+ // send search request(s) to server
+ if (search.query && search.execute) {
+ var sources = [ 'folders' /*, 'users'*/ ];
+ var reqid = rcmail.multi_thread_http_request({
+ items: sources,
+ threads: rcmail.env.autocomplete_threads || 1,
+ action: 'calendar/calendar',
+ postdata: { action:'search', q:search.query, source:'%s' },
+ lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
+ onresponse: calendar_search_results
+ });
+
+ listsearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
+ }
});
// init (delegate) event handler on calendar list checkboxes
$(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e){
- var id = $(this).data('id');
+ var id = this.value;
if (me.calendars[id]) { // add or remove event source on click
var action;
if (this.checked) {
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index c5eff9d..9fb6ffb 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -166,6 +166,15 @@ abstract class calendar_driver
abstract function remove_calendar($prop);
/**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ abstract function search_calendars($query, $source);
+
+ /**
* Add a single event to the database
*
* @param array Hash array with event properties (see header of this file)
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 059cd1d..d0e6b7b 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -248,6 +248,19 @@ class database_driver extends calendar_driver
}
/**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ public function search_calendars($query, $source)
+ {
+ // not implemented
+ return array();
+ }
+
+ /**
* Add a single event to the database
*
* @param array Hash array with event properties
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 46fbed9..5feed23 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -52,11 +52,12 @@ class kolab_calendar
$this->imap_folder = $this->name = $imap_folder;
// ID is derrived from folder name
- $this->id = kolab_storage::folder_id($this->imap_folder);
+ $this->id = kolab_storage::folder_id($this->imap_folder, true);
+ $old_id = kolab_storage::folder_id($this->imap_folder, false);
// fetch objects from the given IMAP folder
$this->storage = kolab_storage::get_folder($this->imap_folder);
- $this->ready = $this->storage && !PEAR::isError($this->storage);
+ $this->ready = $this->storage && !PEAR::isError($this->storage) && $this->storage->type !== null;
// Set readonly and alarms flags according to folder permissions
if ($this->ready) {
@@ -76,6 +77,8 @@ class kolab_calendar
$prefs = $this->cal->rc->config->get('kolab_calendars', array());
if (isset($prefs[$this->id]['showalarms']))
$this->alarms = $prefs[$this->id]['showalarms'];
+ else if (isset($prefs[$old_id]['showalarms']))
+ $this->alarms = $prefs[$old_id]['showalarms'];
}
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 974a3d3..786556f 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -62,6 +62,9 @@ class kolab_driver extends calendar_driver
$this->alarm_types = array('DISPLAY');
$this->alarm_absolute = false;
}
+
+ // calendar uses fully encoded identifiers
+ kolab_storage::$encode_ids = true;
}
@@ -117,6 +120,9 @@ class kolab_driver extends calendar_driver
foreach ($folders as $id => $cal) {
$fullname = $cal->get_name();
$listname = kolab_storage::folder_displayname($fullname, $names);
+ $imap_path = explode('/', $cal->name);
+ $topname = array_pop($imap_path);
+ $parent_id = kolab_storage::folder_id(join('/', $imap_path), true);
// special handling for virtual folders
if ($cal->virtual) {
@@ -143,6 +149,7 @@ class kolab_driver extends calendar_driver
'active' => $cal->storage->is_active(),
'owner' => $cal->get_owner(),
'children' => true, // TODO: determine if that folder indeed has child folders
+ 'parent' => $parent_id,
'caldavurl' => $cal->get_caldav_url(),
);
}
@@ -212,6 +219,26 @@ class kolab_driver extends calendar_driver
return $calendars;
}
+
+ /**
+ * Get the kolab_calendar instance for the given calendar ID
+ *
+ * @param string Calendar identifier (encoded imap folder name)
+ * @return object kolab_calendar Object nor null if calendar doesn't exist
+ */
+ protected function get_calendar($id)
+ {
+ // create calendar object if necesary
+ if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
+ $foldername = kolab_storage::id_decode($id);
+ $calendar = new kolab_calendar($foldername, $this->cal);
+ if ($calendar->ready)
+ $this->calendars[$calendar->id] = $calendar;
+ }
+
+ return $this->calendars[$id];
+ }
+
/**
* Create a new calendar assigned to the current user
*
@@ -257,7 +284,7 @@ class kolab_driver extends calendar_driver
*/
public function edit_calendar($prop)
{
- if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
+ if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
$prop['oldname'] = $cal->get_realname();
$newfolder = kolab_storage::folder_update($prop);
@@ -297,9 +324,10 @@ class kolab_driver extends calendar_driver
*
* @see calendar_driver::subscribe_calendar()
*/
- public function subscribe_calendar($prop)
+ public function subscribe_calendar($prop, $permanent = false)
{
- if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
+ if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
+ if ($permanent) $cal->storage->subscribe($prop['active']);
return $cal->storage->activate($prop['active']);
}
else {
@@ -321,8 +349,9 @@ class kolab_driver extends calendar_driver
*/
public function remove_calendar($prop)
{
- if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
+ if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
$folder = $cal->get_realname();
+ // TODO: unsubscribe if no admin rights
if (kolab_storage::folder_delete($folder)) {
// remove color in user prefs (temp. solution)
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
@@ -340,6 +369,51 @@ class kolab_driver extends calendar_driver
/**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ public function search_calendars($query, $source)
+ {
+ if (!kolab_storage::setup())
+ return array();
+
+ $this->calendars = array();
+ $imap = $this->rc->get_storage();
+
+ // find unsubscribed IMAP folders that have "event" type
+ if ($source == 'folders') {
+ $folders = array();
+ foreach ((array)kolab_storage::list_folders('', '*', 'event', false, $folderdata) as $foldername) {
+ // FIXME: only consider the last part of the folder path for searching?
+ $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
+ if (strpos($realname, $query) !== false &&
+ !kolab_storage::folder_is_subscribed($foldername, true) &&
+ $imap->folder_namespace($foldername) != 'other'
+ ) {
+ $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ }
+ }
+
+ foreach ($folders as $folder) {
+ $calendar = new kolab_calendar($folder->name, $this->cal);
+ $this->calendars[$calendar->id] = $calendar;
+ }
+ }
+ else if ($source == 'users') {
+ // TODO: implement this
+ }
+
+ // don't list the birthday calendar
+ $this->rc->config->set('calendar_contact_birthdays', false);
+
+ return $this->list_calendars();
+ }
+
+
+ /**
* Fetch a single event
*
* @see calendar_driver::get_event()
@@ -356,7 +430,7 @@ class kolab_driver extends calendar_driver
}
if ($cal) {
- if ($storage = $this->calendars[$cal]) {
+ if ($storage = $this->get_calendar($cal)) {
return $storage->get_event($id);
}
}
@@ -383,7 +457,7 @@ class kolab_driver extends calendar_driver
return false;
$cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
- if ($storage = $this->calendars[$cid]) {
+ if ($storage = $this->get_calendar($cid)) {
// handle attachments to add
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $idx => $attachment) {
@@ -425,7 +499,7 @@ class kolab_driver extends calendar_driver
*/
public function move_event($event)
{
- if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) {
+ if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
return $this->update_event($event + $ev);
}
@@ -441,7 +515,7 @@ class kolab_driver extends calendar_driver
*/
public function resize_event($event)
{
- if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) {
+ if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
unset($ev['sequence']);
return $this->update_event($event + $ev);
}
@@ -463,7 +537,7 @@ class kolab_driver extends calendar_driver
$success = false;
$savemode = $event['_savemode'];
- if (($storage = $this->calendars[$event['calendar']]) && ($event = $storage->get_event($event['id']))) {
+ if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
$event['_savemode'] = $savemode;
$savemode = 'all';
$master = $event;
@@ -566,7 +640,7 @@ class kolab_driver extends calendar_driver
*/
public function restore_event($event)
{
- if ($storage = $this->calendars[$event['calendar']]) {
+ if ($storage = $this->get_calendar($event['calendar'])) {
if (!empty($_SESSION['calendar_restore_event_data']))
$success = $storage->update_event($_SESSION['calendar_restore_event_data']);
else
@@ -586,12 +660,12 @@ class kolab_driver extends calendar_driver
*/
private function update_event($event)
{
- if (!($storage = $this->calendars[$event['calendar']]))
+ if (!($storage = $this->get_calendar($event['calendar'])))
return false;
// move event to another folder/calendar
if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
- if (!($fromcalendar = $this->calendars[$event['_fromcalendar']]))
+ if (!($fromcalendar = $this->get_calendar($event['_fromcalendar'])))
return false;
if ($event['_savemode'] != 'new') {
@@ -780,18 +854,19 @@ class kolab_driver extends calendar_driver
{
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
+ else if (!$calendars)
+ $calendars = array_keys($this->calendars);
$query = array();
if ($modifiedsince)
$query[] = array('changed', '>=', $modifiedsince);
$events = $categories = array();
- foreach (array_keys($this->calendars) as $cid) {
- if ($calendars && !in_array($cid, $calendars))
- continue;
-
- $events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual, $query));
- $categories += $this->calendars[$cid]->categories;
+ foreach ($calendars as $cid) {
+ if ($storage = $this->get_calendar($cid)) {
+ $events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query));
+ $categories += $storage->categories;
+ }
}
// add events from the address books birthday calendar
@@ -928,7 +1003,7 @@ class kolab_driver extends calendar_driver
*/
public function list_attachments($event)
{
- if (!($storage = $this->calendars[$event['calendar']]))
+ if (!($storage = $this->get_calendar($event['calendar'])))
return false;
$event = $storage->get_event($event['id']);
@@ -941,7 +1016,7 @@ class kolab_driver extends calendar_driver
*/
public function get_attachment($id, $event)
{
- if (!($storage = $this->calendars[$event['calendar']]))
+ if (!($storage = $this->get_calendar($event['calendar'])))
return false;
$event = $storage->get_event($event['id']);
@@ -963,7 +1038,7 @@ class kolab_driver extends calendar_driver
*/
public function get_attachment_body($id, $event)
{
- if (!($cal = $this->calendars[$event['calendar']]))
+ if (!($cal = $this->get_calendar($event['calendar'])))
return false;
return $cal->storage->get_attachment($event['id'], $id);
@@ -1080,7 +1155,7 @@ class kolab_driver extends calendar_driver
ignore_user_abort(true);
$cal = get_input_value('source', RCUBE_INPUT_GPC);
- if (!($cal = $this->calendars[$cal]))
+ if (!($cal = $this->get_calendar($cal)))
return false;
// trigger updates on folder
@@ -1275,7 +1350,7 @@ class kolab_driver extends calendar_driver
public function calendar_acl_form()
{
$calid = get_input_value('_id', RCUBE_INPUT_GPC);
- if ($calid && ($cal = $this->calendars[$calid])) {
+ if ($calid && ($cal = $this->get_calendar($calid))) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 117b924..69ba9c2 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -157,26 +157,7 @@ class calendar_ui
foreach ((array)$calendars as $id => $prop) {
if (!$prop['color'])
continue;
- $color = $prop['color'];
- $class = 'cal-' . asciiwords($id, true);
- $css .= "li.$class, #eventshow .$class { color: #$color }\n";
- if ($mode != 1) {
- if ($mode == 3) {
- $css .= ".fc-event-$class .fc-event-bg {";
- $css .= " opacity: 0.9;";
- $css .= " filter: alpha(opacity=90);";
- }
- else {
- $css .= ".fc-event-$class, ";
- $css .= ".fc-event-$class .fc-event-inner {";
- }
- if (!$attrib['printmode'])
- $css .= " background-color: #$color;";
- if ($mode % 2 == 0)
- $css .= " border-color: #$color;";
- $css .= "}\n";
- }
- $css .= ".$class .handle { background-color: #$color; }";
+ $css .= $this->calendar_css_classes($id, $prop, $mode);
}
return html::tag('style', array('type' => 'text/css'), $css);
@@ -185,6 +166,35 @@ class calendar_ui
/**
*
*/
+ public function calendar_css_classes($id, $prop, $mode)
+ {
+ $color = $prop['color'];
+ $class = 'cal-' . asciiwords($id, true);
+ $css .= "li.$class, #eventshow .$class { color: #$color }\n";
+
+ if ($mode != 1) {
+ if ($mode == 3) {
+ $css .= ".fc-event-$class .fc-event-bg {";
+ $css .= " opacity: 0.9;";
+ $css .= " filter: alpha(opacity=90);";
+ }
+ else {
+ $css .= ".fc-event-$class, ";
+ $css .= ".fc-event-$class .fc-event-inner {";
+ }
+ if (!$attrib['printmode'])
+ $css .= " background-color: #$color;";
+ if ($mode % 2 == 0)
+ $css .= " border-color: #$color;";
+ $css .= "}\n";
+ }
+
+ return $css . ".$class .handle { background-color: #$color; }\n";
+ }
+
+ /**
+ *
+ */
function calendar_list($attrib = array())
{
$html = '';
@@ -222,9 +232,9 @@ class calendar_ui
return html::tag('ul', $attrib, $html, html::$common_attrib);
}
-
+
/**
- * Return html for a structured list <ul> for the mailbox tree
+ * Return html for a structured list <ul> for the folder tree
*/
public function list_tree_html(&$node, &$data, &$jsenv, $attrib)
{
@@ -255,7 +265,7 @@ class calendar_ui
/**
* Helper method to build a calendar list item (HTML content and js data)
*/
- protected function calendar_list_item($id, $prop, &$jsenv)
+ public function calendar_list_item($id, $prop, &$jsenv)
{
unset($prop['user_id']);
$prop['alarms'] = $this->cal->driver->alarms;
@@ -283,7 +293,7 @@ class calendar_ui
if (!$attrib['activeonly'] || $prop['active']) {
$content = html::div($class,
($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
- html::span('handle', ' ')) .
+ html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')) .
html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname'])
);
}
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 4621feb..b6e7ff2 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -86,6 +86,7 @@ $labels['nmonthsback'] = '$nr months back';
$labels['showurl'] = 'Show calendar URL';
$labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
+$labels['calsearchresults'] = 'Additional Results';
// agenda view
$labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 1ffaea8..01a4c3d 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -168,27 +168,23 @@ pre {
top: 68px;
}
-#calendarslist li {
+#calendars .treelist li {
margin: 0;
position: relative;
}
-#calendarslist li label {
- display: block;
-}
-
-#calendarslist li div.folder,
-#calendarslist li div.calendar {
+#calendars .treelist li div.folder,
+#calendars .treelist li div.calendar {
position: relative;
height: 28px;
}
-#calendarslist li div.virtual {
+#calendars .treelist li div.virtual {
height: 22px;
}
-#calendarslist li span.calname {
+#calendars .treelist li span.calname {
display: block;
padding: 0px 30px 2px 2px;
position: absolute;
@@ -203,17 +199,17 @@ pre {
color: #004458;
}
-#calendarslist li div.virtual > span.calname {
+#calendars .treelist li div.virtual > span.calname {
color: #aaa;
top: 4px;
left: 20px;
}
-#calendarslist.flat li span.calname {
+#calendars .treelist.flat li span.calname {
left: 24px;
}
-#calendarslist li span.handle {
+#calendars .treelist li span.handle {
display: inline-block;
position: absolute;
top: 8px;
@@ -229,66 +225,77 @@ pre {
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
}
-#calendarslist li input {
+#calendars .treelist li input {
position: absolute;
top: 5px;
left: 18px;
}
-#calendarslist li div.treetoggle {
+#calendars .treelist li div.treetoggle {
top: 8px;
}
-#calendarslist li.virtual div.treetoggle {
+#calendars .treelist li.virtual div.treetoggle {
top: 6px;
}
-#calendarslist.flat li input {
+#calendars .treelist.flat li input {
left: 4px;
}
-#calendarslist ul li div.folder,
-#calendarslist ul li div.calendar {
+#calendars .treelist ul li div.folder,
+#calendars .treelist ul li div.calendar {
margin-left: 16px;
}
-#calendarslist ul ul li div.folder,
-#calendarslist ul ul li div.calendar {
+#calendars .treelist ul ul li div.folder,
+#calendars .treelist ul ul li div.calendar {
margin-left: 32px;
}
-#calendarslist ul ul ul li div.folder,
-#calendarslist ul ul ul li div.calendar {
+#calendars .treelist ul ul ul li div.folder,
+#calendars .treelist ul ul ul li div.calendar {
margin-left: 48px;
}
-#calendarslist li.selected {
+#calendars .treelist li.selected {
background-color: #c7e3ef;
}
-#calendarslist li.selected > span.calname {
+#calendars .treelist li.selected > span.calname {
font-weight: bold;
}
-#calendarslist div.readonly span.calname {
+#calendars .treelist div.readonly span.calname {
background-position: right -20px;
}
-
-#calendarslist div.other span.calname {
+/*
+#calendars .treelist div.other span.calname {
background-position: right -38px;
}
-#calendarslist div.other.readonly span.calname {
+#calendars .treelist div.other.readonly span.calname {
background-position: right -56px;
}
-#calendarslist div.shared span.calname {
+#calendars .treelist div.shared span.calname {
background-position: right -74px;
}
-#calendarslist div.shared.readonly span.calname {
+#calendars .treelist div.shared.readonly span.calname {
background-position: right -92px;
}
+*/
+
+#calendars .searchresults {
+ background: #b0ccd7;
+ margin-top: 8px;
+}
+
+#calendars .searchresults .boxtitle {
+ background: none;
+ padding: 2px 8px 2px 8px;
+}
#calfeedurl,
#caldavurl {
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index c8dd59e..f28d496 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -247,7 +247,7 @@ class kolab_addressbook extends rcube_plugin
$names = array();
foreach ($folders as $folder) {
// create instance of rcube_contacts
- $abook_id = kolab_storage::folder_id($folder->name);
+ $abook_id = kolab_storage::folder_id($folder->name, false);
$abook = new rcube_kolab_contacts($folder->name);
$this->sources[$abook_id] = $abook;
}
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 9504e8f..55526fd 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -7,7 +7,7 @@
* @author Thomas Bruederli <bruederli at kolabsys.com>
* @author Aleksander Machniak <machniak at kolabsys.com>
*
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -37,6 +37,7 @@ class kolab_storage
public static $version = '3.0';
public static $last_error;
+ public static $encode_ids = false;
private static $ready = false;
private static $subscriptions;
@@ -226,13 +227,39 @@ class kolab_storage
/**
* Creates folder ID from folder name
*
- * @param string $folder Folder name (UTF7-IMAP)
- *
+ * @param string $folder Folder name (UTF7-IMAP)
+ * @param boolean $enc Use lossless encoding
* @return string Folder ID string
*/
- public static function folder_id($folder)
+ public static function folder_id($folder, $enc = null)
{
- return asciiwords(strtr($folder, '/.-', '___'));
+ return $enc == true || ($enc === null && self::$encode_ids) ?
+ self::id_encode($folder) :
+ asciiwords(strtr($folder, '/.-', '___'));
+ }
+
+
+ /**
+ * Encode the given ID to a safe ascii representation
+ *
+ * @param string $id Arbitrary identifier string
+ *
+ * @return string Ascii representation
+ */
+ public static function id_encode($id)
+ {
+ return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
+ }
+
+ /**
+ * Convert the given identifier back to it's raw value
+ *
+ * @param string $id Ascii identifier
+ * @return string Raw identifier string
+ */
+ public static function id_decode($id)
+ {
+ return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
}
@@ -665,11 +692,16 @@ class kolab_storage
if (!$filter) {
// Get ALL folders list, standard way
if ($subscribed) {
- return self::$imap->list_folders_subscribed($root, $mbox);
+ $folders = self::$imap->list_folders_subscribed($root, $mbox);
+ // add temporarily subscribed folders
+ if (is_array($_SESSION['kolab_subscribed_folders']))
+ $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
}
else {
- return self::$imap->list_folders($root, $mbox);
+ $folders = self::$imap->list_folders($root, $mbox);
}
+
+ return $folders;
}
$prefix = $root . $mbox;
@@ -696,6 +728,10 @@ class kolab_storage
// Get folders list
if ($subscribed) {
$folders = self::$imap->list_folders_subscribed($root, $mbox);
+
+ // add temporarily subscribed folders
+ if (is_array($_SESSION['kolab_subscribed_folders']))
+ $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
}
else {
$folders = self::$imap->list_folders($root, $mbox);
@@ -903,17 +939,19 @@ class kolab_storage
* Check subscription status of this folder
*
* @param string $folder Folder name
+ * @param boolean $temp Include temporary/session subscriptions
*
* @return boolean True if subscribed, false if not
*/
- public static function folder_is_subscribed($folder)
+ public static function folder_is_subscribed($folder, $temp = false)
{
if (self::$subscriptions === null) {
self::setup();
self::$subscriptions = self::$imap->list_folders_subscribed();
}
- return in_array($folder, self::$subscriptions);
+ return in_array($folder, self::$subscriptions) ||
+ ($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
}
@@ -921,14 +959,25 @@ class kolab_storage
* Change subscription status of this folder
*
* @param string $folder Folder name
+ * @param boolean $temp Only subscribe temporarily for the current session
*
* @return True on success, false on error
*/
- public static function folder_subscribe($folder)
+ public static function folder_subscribe($folder, $temp = false)
{
self::setup();
- if (self::$imap->subscribe($folder)) {
+ // temporary/session subscription
+ if ($temp) {
+ if (self::folder_is_subscribed($folder)) {
+ return true;
+ }
+ else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
+ $_SESSION['kolab_subscribed_folders'][] = $folder;
+ return true;
+ }
+ }
+ else if (self::$imap->subscribe($folder)) {
self::$subscriptions === null;
return true;
}
@@ -981,6 +1030,8 @@ class kolab_storage
*/
public static function folder_activate($folder)
{
+ // activation implies temporary subscription
+ self::folder_subscribe($folder, true);
return self::set_state($folder, true);
}
@@ -994,6 +1045,11 @@ class kolab_storage
*/
public static function folder_deactivate($folder)
{
+ // remove from temp subscriptions
+ if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
+ unset($_SESSION['kolab_subscribed_folders'][$i]);
+ }
+
return self::set_state($folder, false);
}
commit 00b1c7631bd8bf00061a77d9363464bcf03dfa49
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Mon May 12 20:47:47 2014 +0200
Render calendar folders as a searchable treelist widget
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 116b490..2a8a8d2 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -51,6 +51,7 @@ function rcube_calendar_ui(settings)
var ignore_click = false;
var event_defaults = { free_busy:'busy', alarms:'' };
var event_attendees = [];
+ var calendars_list;
var attendees_list;
var resources_list;
var resources_treelist;
@@ -2658,15 +2659,10 @@ function rcube_calendar_ui(settings)
// mark the given calendar folder as selected
this.select_calendar = function(id)
{
- var prefix = 'rcmlical';
-
- $(rcmail.gui_objects.calendarslist).find('li.selected')
- .removeClass('selected').addClass('unfocused');
- $('#' + prefix + id, rcmail.gui_objects.calendarslist)
- .removeClass('unfocused').addClass('selected');
+ calendars_list.select(id);
// trigger event hook
- rcmail.triggerEvent('selectfolder', { folder:name, prefix:prefix });
+ rcmail.triggerEvent('selectfolder', { folder:id, prefix:'rcmlical' });
this.selected_calendar = id;
};
@@ -2703,42 +2699,58 @@ function rcube_calendar_ui(settings)
event_sources.push(this.calendars[id]);
}
- // init event handler on calendar list checkbox
- if ((li = rcube_find_object('rcmlical' + id))) {
- $('#'+li.id+' input').click(function(e){
- var id = $(this).data('id');
- if (me.calendars[id]) { // add or remove event source on click
- var action;
- if (this.checked) {
- action = 'addEventSource';
- me.calendars[id].active = true;
- }
- else {
- action = 'removeEventSource';
- me.calendars[id].active = false;
- }
-
- // add/remove event source
- fc.fullCalendar(action, me.calendars[id]);
- rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
- }
- }).data('id', id).get(0).checked = active;
-
- $(li).click(function(e){
- me.select_calendar($(this).data('id'));
- rcmail.enable_command('calendar-edit', true);
- rcmail.enable_command('calendar-remove', 'calendar-showurl', true);
- })
- .dblclick(function(){ me.calendar_edit_dialog(me.calendars[me.selected_calendar]); })
- .data('id', id);
- }
-
+ // check active calendars
+ $('#rcmlical'+id+' > .calendar input').data('id', id).get(0).checked = active;
+
if (!cal.readonly && !this.selected_calendar) {
this.selected_calendar = id;
rcmail.enable_command('addevent', true);
}
}
-
+
+ // initialize treelist widget that controls the calendars list
+ calendars_list = new rcube_treelist_widget(rcmail.gui_objects.calendarslist, {
+ id_prefix: 'rcmlical',
+ selectable: true,
+ searchbox: '#calendarlistsearch'
+ });
+ calendars_list.addEventListener('select', function(node){
+ me.select_calendar(node.id);
+ rcmail.enable_command('calendar-edit', 'calendar-showurl', true);
+ rcmail.enable_command('calendar-remove', !me.calendars[node.id].readonly);
+ });
+ calendars_list.addEventListener('search', function(search){
+ console.log(search);
+ });
+
+ // init (delegate) event handler on calendar list checkboxes
+ $(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e){
+ var id = $(this).data('id');
+ if (me.calendars[id]) { // add or remove event source on click
+ var action;
+ if (this.checked) {
+ action = 'addEventSource';
+ me.calendars[id].active = true;
+ }
+ else {
+ action = 'removeEventSource';
+ me.calendars[id].active = false;
+ }
+
+ // add/remove event source
+ fc.fullCalendar(action, me.calendars[id]);
+ rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
+
+ e.stopPropagation();
+ }
+ });
+
+ // register dbl-click handler to open calendar edit dialog
+ $(rcmail.gui_objects.calendarslist).on('dblclick', ':not(.virtual) > .calname', function(e){
+ var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
+ me.calendar_edit_dialog(me.calendars[id]);
+ });
+
// select default calendar
if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly)
this.selected_calendar = settings.default_calendar;
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 28eb8ba..974a3d3 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -88,16 +88,16 @@ class kolab_driver extends calendar_driver
return $this->calendars;
}
-
/**
* Get a list of available calendars from this source
*
* @param bool $active Return only active calendars
* @param bool $personal Return only personal calendars
+ * @param object $tree Reference to hierarchical folder tree object
*
* @return array List of calendars
*/
- public function list_calendars($active = false, $personal = false)
+ public function list_calendars($active = false, $personal = false, &$tree = null)
{
// attempt to create a default calendar for this user
if (!$this->has_writeable) {
@@ -112,7 +112,7 @@ class kolab_driver extends calendar_driver
// include virtual folders for a full folder tree
if (!$active && !$personal && !$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
- $folders = kolab_storage::folder_hierarchy($folders);
+ $folders = kolab_storage::folder_hierarchy($folders, $tree);
foreach ($folders as $id => $cal) {
$fullname = $cal->get_name();
@@ -124,6 +124,7 @@ class kolab_driver extends calendar_driver
'id' => $cal->id,
'name' => $fullname,
'listname' => $listname,
+ 'editname' => $cal->get_foldername(),
'virtual' => true,
'readonly' => true,
);
@@ -1106,6 +1107,11 @@ class kolab_driver extends calendar_driver
*/
public function calendar_form($action, $calendar, $formfields)
{
+ // show default dialog for birthday calendar
+ if ($calendar['id'] == self::BIRTHDAY_CALENDAR_ID) {
+ return parent::calendar_form($action, $calendar, $formfields);
+ }
+
if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 984ce03..117b924 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -187,45 +187,108 @@ class calendar_ui
*/
function calendar_list($attrib = array())
{
- $calendars = $this->cal->driver->list_calendars();
+ $html = '';
+ $jsenv = array();
+ $calendars = $this->cal->driver->list_calendars(false, false, $tree);
+
+ // walk folder tree
+ if (is_object($tree)) {
+ $html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib);
+
+ // append birthdays calendar which isn't part of $tree
+ if ($bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID]) {
+ $calendars = array(calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal);
+ }
+ else {
+ $calendars = array(); // clear array for flat listing
+ }
+ }
+ else {
+ // fall-back to flat folder listing
+ $attrib['class'] .= ' flat';
+ }
- $li = '';
foreach ((array)$calendars as $id => $prop) {
if ($attrib['activeonly'] && !$prop['active'])
continue;
-
- unset($prop['user_id']);
- $prop['alarms'] = $this->cal->driver->alarms;
- $prop['attendees'] = $this->cal->driver->attendees;
- $prop['freebusy'] = $this->cal->driver->freebusy;
- $prop['attachments'] = $this->cal->driver->attachments;
- $prop['undelete'] = $this->cal->driver->undelete;
- $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
-
- if (!$prop['virtual'])
- $jsenv[$id] = $prop;
-
- $html_id = html_identifier($id);
- $class = 'cal-' . asciiwords($id, true);
- $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
-
- if ($prop['virtual'])
- $class .= ' virtual';
- else if ($prop['readonly'])
- $class .= ' readonly';
- if ($prop['class_name'])
- $class .= ' '.$prop['class_name'];
-
- $li .= html::tag('li', array('id' => 'rcmlical' . $html_id, 'class' => $class),
- ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
- html::span('handle', ' ')) .
- html::span(array('class' => 'calname', 'title' => $title), $prop['listname']));
+
+ $html .= html::tag('li', array('id' => 'rcmlical' . rcube_utils::html_identifier($id)),
+ $content = $this->calendar_list_item($id, $prop, $jsenv)
+ );
}
$this->rc->output->set_env('calendars', $jsenv);
$this->rc->output->add_gui_object('calendarslist', $attrib['id']);
- return html::tag('ul', $attrib, $li, html::$common_attrib);
+ return html::tag('ul', $attrib, $html, html::$common_attrib);
+ }
+
+ /**
+ * Return html for a structured list <ul> for the mailbox tree
+ */
+ public function list_tree_html(&$node, &$data, &$jsenv, $attrib)
+ {
+ $out = '';
+ foreach ($node->children as $folder) {
+ $id = $folder->id;
+ $prop = $data[$id];
+
+ $content = $this->calendar_list_item($id, $prop, $jsenv);
+
+ if (!empty($folder->children)) {
+ $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
+ $this->list_tree_html($folder, $data, $jsenv, $attrib));
+ }
+
+ if (strlen($content)) {
+ $out .= html::tag('li', array(
+ 'id' => 'rcmlical' . rcube_utils::html_identifier($id),
+ 'class' => $prop['virtual'] ? 'virtual' : '',
+ ),
+ $content);
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Helper method to build a calendar list item (HTML content and js data)
+ */
+ protected function calendar_list_item($id, $prop, &$jsenv)
+ {
+ unset($prop['user_id']);
+ $prop['alarms'] = $this->cal->driver->alarms;
+ $prop['attendees'] = $this->cal->driver->attendees;
+ $prop['freebusy'] = $this->cal->driver->freebusy;
+ $prop['attachments'] = $this->cal->driver->attachments;
+ $prop['undelete'] = $this->cal->driver->undelete;
+ $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
+
+ if (!$prop['virtual'])
+ $jsenv[$id] = $prop;
+
+ $class = 'calendar cal-' . asciiwords($id, true);
+ $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
+ $is_collapsed = false; // TODO: determine this somehow?
+
+ if ($prop['virtual'])
+ $class = 'folder virtual';
+ else if ($prop['readonly'])
+ $class .= ' readonly';
+ if ($prop['class_name'])
+ $class .= ' '.$prop['class_name'];
+
+ $content = '';
+ if (!$attrib['activeonly'] || $prop['active']) {
+ $content = html::div($class,
+ ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
+ html::span('handle', ' ')) .
+ html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname'])
+ );
+ }
+
+ return $content;
}
/**
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index f47032f..1ffaea8 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -164,45 +164,64 @@ pre {
right: 0;
}
+#calendars .scroller {
+ top: 68px;
+}
+
#calendarslist li {
margin: 0;
- height: 20px;
- padding: 6px 8px 2px;
- display: block;
position: relative;
}
-#calendarslist li.virtual {
- height: 12px;
-}
-
#calendarslist li label {
display: block;
}
+#calendarslist li div.folder,
+#calendarslist li div.calendar {
+ position: relative;
+ height: 28px;
+}
+
+#calendarslist li div.virtual {
+ height: 22px;
+}
+
+
#calendarslist li span.calname {
display: block;
+ padding: 0px 30px 2px 2px;
position: absolute;
- top: 6px;
- left: 26px;
- right: 24px;
+ top: 7px;
+ left: 38px;
+ right: 22px;
cursor: default;
background: url(images/calendars.png) right 20px no-repeat;
- padding-bottom: 2px;
- padding-right: 30px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #004458;
}
+#calendarslist li div.virtual > span.calname {
+ color: #aaa;
+ top: 4px;
+ left: 20px;
+}
+
+#calendarslist.flat li span.calname {
+ left: 24px;
+}
+
#calendarslist li span.handle {
display: inline-block;
+ position: absolute;
+ top: 8px;
+ right: 6px;
padding: 0;
- border-radius: 7px;
- margin-right: 6px;
width: 10px;
height: 10px;
+ border-radius: 7px;
font-size: 0.8em;
border: 1px solid rgba(0, 0, 0, 0.5);
-webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
@@ -212,43 +231,65 @@ pre {
#calendarslist li input {
position: absolute;
- top: 4px;
- right: 5px;
+ top: 5px;
+ left: 18px;
+}
+
+#calendarslist li div.treetoggle {
+ top: 8px;
+}
+
+#calendarslist li.virtual div.treetoggle {
+ top: 6px;
+}
+
+#calendarslist.flat li input {
+ left: 4px;
+}
+
+#calendarslist ul li div.folder,
+#calendarslist ul li div.calendar {
+ margin-left: 16px;
+}
+
+#calendarslist ul ul li div.folder,
+#calendarslist ul ul li div.calendar {
+ margin-left: 32px;
+}
+
+#calendarslist ul ul ul li div.folder,
+#calendarslist ul ul ul li div.calendar {
+ margin-left: 48px;
}
#calendarslist li.selected {
background-color: #c7e3ef;
}
-#calendarslist li.selected span.calname {
+#calendarslist li.selected > span.calname {
font-weight: bold;
}
-#calendarslist li.readonly span.calname {
+#calendarslist div.readonly span.calname {
background-position: right -20px;
}
-#calendarslist li.other span.calname {
+#calendarslist div.other span.calname {
background-position: right -38px;
}
-#calendarslist li.other.readonly span.calname {
+#calendarslist div.other.readonly span.calname {
background-position: right -56px;
}
-#calendarslist li.shared span.calname {
+#calendarslist div.shared span.calname {
background-position: right -74px;
}
-#calendarslist li.shared.readonly span.calname {
+#calendarslist div.shared.readonly span.calname {
background-position: right -92px;
}
-#calendarslist li.virtual span.calname {
- color: #aaa;
- top: 2px;
-}
-
#calfeedurl,
#caldavurl {
width: 98%;
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index debe7ec..0842cf0 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -23,8 +23,15 @@
<div id="calendars" class="uibox listbox" style="visibility:hidden">
<h2 class="boxtitle"><roundcube:label name="calendar.calendars" /></h2>
+ <div class="listsearchbox">
+ <div class="searchbox">
+ <input type="text" name="q" id="calendarlistsearch" />
+ <a class="iconbutton searchicon"></a>
+ <roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
+ </div>
+ </div>
<div class="scroller withfooter">
- <roundcube:object name="plugin.calendar_list" id="calendarslist" class="listing" />
+ <roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist listing" />
</div>
<div class="boxfooter">
<roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="calendaroptionslink" id="calendaroptionsmenulink" type="link" title="moreactions" class="listbutton groupactions" onclick="UI.show_popup('calendaroptionsmenu', undefined, { above:true });return false" innerClass="inner" content="⚙" />
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 52df016..9504e8f 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -756,43 +756,54 @@ class kolab_storage
* Check the folder tree and add the missing parents as virtual folders
*
* @param array $folders Folders list
+ * @param object $tree Reference to the root node of the folder tree
*
- * @return array Folders list
+ * @return array Flat folders list
*/
- public static function folder_hierarchy($folders)
+ public static function folder_hierarchy($folders, &$tree)
{
$_folders = array();
- $existing = array_map(function($folder){ return $folder->get_name(); }, $folders);
$delim = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
+ $tree = new virtual_kolab_storage_folder('', '<root>', ''); // create tree root
+ $refs = array('' => $tree);
foreach ($folders as $idx => $folder) {
$path = explode($delim, $folder->name);
array_pop($path);
+ $folder->parent = join($delim, $path);
+ $folder->children = array(); // reset list
// skip top folders or ones with a custom displayname
- if (count($path) <= 1 || kolab_storage::custom_displayname($folder->name)) {
+ if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
+ $tree->children[] = $folder;
}
else {
$parents = array();
+ $depth = $folder->get_namespace() == 'personal' ? 1 : 2;
- while (count($path) > 1 && ($parent = join($delim, $path))) {
+ while (count($path) >= $depth && ($parent = join($delim, $path))) {
+ array_pop($path);
$name = kolab_storage::object_name($parent, $folder->get_namespace());
- if (!in_array($name, $existing)) {
- $parents[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace());
- $existing[] = $name;
+ if (!$refs[$parent]) {
+ $refs[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace(), join($delim, $path));
+ $parents[] = $refs[$parent];
}
-
- array_pop($path);
}
if (!empty($parents)) {
- $parents = array_reverse(array_values($parents));
+ $parents = array_reverse($parents);
foreach ($parents as $parent) {
+ $parent_node = $refs[$parent->parent] ?: $tree;
+ $parent_node->children[] = $parent;
$_folders[] = $parent;
}
}
+
+ $parent_node = $refs[$folder->parent] ?: $tree;
+ $parent_node->children[] = $folder;
}
+ $refs[$folder->name] = $folder;
$_folders[] = $folder;
unset($folders[$idx]);
}
@@ -1164,13 +1175,18 @@ class virtual_kolab_storage_folder
public $id;
public $name;
public $namespace;
+ public $parent = '';
+ public $children = array();
public $virtual = true;
+ protected $displayname;
- public function __construct($realname, $name, $ns)
+ public function __construct($name, $dispname, $ns, $parent = '')
{
- $this->id = kolab_storage::folder_id($realname);
+ $this->id = kolab_storage::folder_id($name);
$this->name = $name;
$this->namespace = $ns;
+ $this->parent = $parent;
+ $this->displayname = $dispname;
}
public function get_namespace()
@@ -1181,6 +1197,12 @@ class virtual_kolab_storage_folder
public function get_name()
{
// this is already kolab_storage::object_name() result
- return $this->name;
+ return $this->displayname;
+ }
+
+ public function get_foldername()
+ {
+ $parts = explode('/', $this->name);
+ return rcube_charset::convert(end($parts), 'UTF7-IMAP');
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index d625cc5..7db493d 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -47,6 +47,12 @@ class kolab_storage_folder
* @var object
*/
public $cache;
+
+ /**
+ * List of direct child folders
+ * @var array
+ */
+ public $children = array();
private $type_annotation;
private $namespace;
@@ -218,6 +224,18 @@ class kolab_storage_folder
/**
+ * Getter for the top-end folder name (not the entire path)
+ *
+ * @return string Name of this folder
+ */
+ public function get_foldername()
+ {
+ $parts = explode('/', $this->name);
+ return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+ }
+
+
+ /**
* Get the color value stored in metadata
*
* @param string Default color value to return if not set
More information about the commits
mailing list