Merge new folder navigation from branch 'dev/new-foldernav'
|
@ -728,6 +728,30 @@ class calendar extends rcube_plugin
|
|||
if (!$this->driver->subscribe_calendar($cal))
|
||||
$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;
|
||||
}
|
||||
// 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)
|
||||
|
@ -2086,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;
|
||||
|
|
|
@ -39,6 +39,7 @@ function rcube_calendar_ui(settings)
|
|||
this.selected_calendar = null;
|
||||
this.search_request = null;
|
||||
this.saving_lock;
|
||||
this.calendars = {};
|
||||
|
||||
|
||||
/*** private vars ***/
|
||||
|
@ -51,6 +52,10 @@ function rcube_calendar_ui(settings)
|
|||
var ignore_click = false;
|
||||
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;
|
||||
|
@ -1543,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]) {
|
||||
|
@ -2658,29 +2664,21 @@ 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;
|
||||
};
|
||||
|
||||
// register the given calendar to the current view
|
||||
var add_calendar_source = function(cal)
|
||||
{
|
||||
var color, brightness, select, id = cal.id;
|
||||
|
||||
/*** startup code ***/
|
||||
|
||||
// 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),
|
||||
me.calendars[id] = $.extend({
|
||||
url: rcmail.url('calendar/load_events', { source: id }),
|
||||
editable: !cal.readonly,
|
||||
className: 'fc-event-cal-'+id,
|
||||
id: id
|
||||
|
@ -2693,52 +2691,117 @@ 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 || cal.subscribed)) {
|
||||
if (cal.active)
|
||||
fc.fullCalendar('addEventSource', me.calendars[id]);
|
||||
|
||||
if ((active = cal.active || false)) {
|
||||
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
|
||||
select = $('#edit-calendar');
|
||||
if (fc && !cal.readonly && select.length && !select.find('option[value="'+id+'"]').length) {
|
||||
$('<option>').attr('value', id).html(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').get(0).checked = active;
|
||||
|
||||
if (active) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (!cal.readonly && !this.selected_calendar) {
|
||||
this.selected_calendar = id;
|
||||
rcmail.enable_command('addevent', true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// initialize treelist widget that controls the calendars list
|
||||
var widget_class = window.kolab_folderlist || rcube_treelist_widget;
|
||||
calendars_list = new widget_class(rcmail.gui_objects.calendarslist, {
|
||||
id_prefix: 'rcmlical',
|
||||
selectable: true,
|
||||
save_state: true,
|
||||
searchbox: '#calendarlistsearch',
|
||||
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);
|
||||
rcmail.enable_command('calendar-edit', 'calendar-showurl', true);
|
||||
rcmail.enable_command('calendar-remove', !me.calendars[node.id].readonly);
|
||||
});
|
||||
calendars_list.addEventListener('insert-item', function(p) {
|
||||
var cal = p.data;
|
||||
if (cal && cal.id) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
||||
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){
|
||||
var id = this.value;
|
||||
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;
|
||||
|
|
|
@ -165,6 +165,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
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
@ -247,6 +247,19 @@ class database_driver extends calendar_driver
|
|||
return $this->rc->db->affected_rows($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
||||
* @author Aleksander Machniak <machniak@kolabsys.com>
|
||||
*
|
||||
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
|
||||
* Copyright (C) 2012-2014, Kolab Systems AG <contact@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
|
||||
|
@ -24,22 +24,41 @@
|
|||
*/
|
||||
|
||||
|
||||
class kolab_calendar
|
||||
class kolab_calendar extends kolab_storage_folder_api
|
||||
{
|
||||
public $id;
|
||||
public $ready = false;
|
||||
public $readonly = true;
|
||||
public $attachments = true;
|
||||
public $alarms = false;
|
||||
public $subscriptions = true;
|
||||
public $categories = array();
|
||||
public $storage;
|
||||
public $name;
|
||||
|
||||
private $cal;
|
||||
private $events = array();
|
||||
private $imap_folder = 'INBOX/Calendar';
|
||||
private $search_fields = array('title', 'description', 'location', 'attendees');
|
||||
public $type = 'event';
|
||||
|
||||
protected $cal;
|
||||
protected $events = array();
|
||||
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'] || strpos(kolab_storage::folder_type($imap_folder), 'event') !== 0) {
|
||||
return new kolab_user_calendar($imap_folder, $calendar);
|
||||
}
|
||||
else {
|
||||
return new kolab_calendar($imap_folder, $calendar);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
|
@ -47,16 +66,16 @@ 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);
|
||||
$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->ready = $this->storage && !PEAR::isError($this->storage);
|
||||
$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
|
||||
if ($this->ready) {
|
||||
|
@ -76,20 +95,11 @@ 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'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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->imap_folder, $this->namespace);
|
||||
return $folder;
|
||||
$this->default = $this->storage->default;
|
||||
}
|
||||
|
||||
|
||||
|
@ -100,43 +110,18 @@ class kolab_calendar
|
|||
*/
|
||||
public function get_realname()
|
||||
{
|
||||
return $this->imap_folder;
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Getter for the IMAP folder owner
|
||||
*
|
||||
* @return string Name of the folder owner
|
||||
*/
|
||||
public function get_owner()
|
||||
public function get_title()
|
||||
{
|
||||
return $this->storage->get_owner();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
@ -167,21 +152,32 @@ 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),
|
||||
));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the corresponding kolab_storage_folder instance
|
||||
*/
|
||||
public function get_folder()
|
||||
{
|
||||
return $this->storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -306,6 +302,9 @@ class kolab_calendar
|
|||
}
|
||||
}
|
||||
|
||||
// avoid session race conditions that will loose temporary subscriptions
|
||||
$this->cal->rc->session->nowrite();
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
@ -62,6 +63,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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -75,29 +79,35 @@ 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('event', 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_folder_user)
|
||||
$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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
@ -107,25 +117,64 @@ 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 (!$active && !$personal && !$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);
|
||||
|
||||
foreach ($folders as $id => $cal) {
|
||||
$fullname = $cal->get_name();
|
||||
$listname = kolab_storage::folder_displayname($fullname, $names);
|
||||
$listname = $cal->get_foldername();
|
||||
$imap_path = explode($delim, $cal->name);
|
||||
|
||||
// special handling for virtual folders
|
||||
if ($cal->virtual) {
|
||||
// find parent
|
||||
do {
|
||||
array_pop($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());
|
||||
}
|
||||
|
||||
// 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_folder_user) {
|
||||
$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(),
|
||||
'title' => $cal->get_owner(),
|
||||
'owner' => $cal->get_owner(),
|
||||
'virtual' => false,
|
||||
'readonly' => true,
|
||||
'group' => 'other',
|
||||
'class' => 'user',
|
||||
);
|
||||
}
|
||||
else if ($cal->virtual) {
|
||||
$calendars[$cal->id] = array(
|
||||
'id' => $cal->id,
|
||||
'name' => $fullname,
|
||||
'listname' => $listname,
|
||||
'editname' => $cal->get_foldername(),
|
||||
'virtual' => true,
|
||||
'readonly' => true,
|
||||
'group' => $cal->get_namespace(),
|
||||
'class' => 'folder',
|
||||
);
|
||||
}
|
||||
else {
|
||||
|
@ -134,17 +183,23 @@ 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,
|
||||
'class_name' => $cal->get_namespace(),
|
||||
'default' => $cal->storage->default,
|
||||
'active' => $cal->storage->is_active(),
|
||||
'group' => $cal->get_namespace(),
|
||||
'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,
|
||||
'caldavurl' => $cal->get_caldav_url(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($cal->subscriptions) {
|
||||
$calendars[$cal->id]['subscribed'] = (bool)$cal->is_subscribed();
|
||||
}
|
||||
}
|
||||
|
||||
// append the virtual birthdays calendar
|
||||
|
@ -159,7 +214,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,
|
||||
|
@ -199,7 +254,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') {
|
||||
|
@ -211,6 +266,25 @@ 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) {
|
||||
$calendar = kolab_calendar::factory($id, $this->cal);
|
||||
if ($calendar->ready)
|
||||
$this->calendars[$calendar->id] = $calendar;
|
||||
}
|
||||
|
||||
return $this->calendars[$id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new calendar assigned to the current user
|
||||
*
|
||||
|
@ -256,17 +330,8 @@ class kolab_driver extends calendar_driver
|
|||
*/
|
||||
public function edit_calendar($prop)
|
||||
{
|
||||
if ($prop['id'] && ($cal = $this->calendars[$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);
|
||||
if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
|
||||
$id = $cal->update($prop);
|
||||
}
|
||||
else {
|
||||
$id = $prop['id'];
|
||||
|
@ -298,8 +363,13 @@ class kolab_driver extends calendar_driver
|
|||
*/
|
||||
public function subscribe_calendar($prop)
|
||||
{
|
||||
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
|
||||
return $cal->storage->activate($prop['active']);
|
||||
if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
|
||||
$ret = false;
|
||||
if (isset($prop['permanent']))
|
||||
$ret |= $cal->storage->subscribe(intval($prop['permanent']));
|
||||
if (isset($prop['active']))
|
||||
$ret |= $cal->storage->activate(intval($prop['active']));
|
||||
return $ret;
|
||||
}
|
||||
else {
|
||||
// save state in local prefs
|
||||
|
@ -320,8 +390,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());
|
||||
|
@ -338,6 +409,54 @@ 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();
|
||||
$this->search_more_results = false;
|
||||
|
||||
// find unsubscribed IMAP folders that have "event" type
|
||||
if ($source == 'folders') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
// find other user's virtual calendars
|
||||
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, $count) 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) {
|
||||
$cal = new kolab_calendar($foldername, $this->cal);
|
||||
$this->calendars[$cal->id] = $cal;
|
||||
}
|
||||
}
|
||||
|
||||
if ($count > $limit) {
|
||||
$this->search_more_results = true;
|
||||
}
|
||||
}
|
||||
|
||||
// don't list the birthday calendar
|
||||
$this->rc->config->set('calendar_contact_birthdays', false);
|
||||
|
||||
return $this->list_calendars();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch a single event
|
||||
*
|
||||
|
@ -355,7 +474,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);
|
||||
}
|
||||
}
|
||||
|
@ -382,7 +501,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) {
|
||||
|
@ -424,7 +543,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);
|
||||
}
|
||||
|
@ -440,7 +559,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);
|
||||
}
|
||||
|
@ -462,7 +581,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;
|
||||
|
@ -565,7 +684,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
|
||||
|
@ -585,12 +704,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') {
|
||||
|
@ -779,18 +898,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
|
||||
|
@ -927,7 +1047,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']);
|
||||
|
@ -940,7 +1060,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']);
|
||||
|
@ -962,7 +1082,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);
|
||||
|
@ -1079,7 +1199,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
|
||||
|
@ -1106,6 +1226,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();
|
||||
|
@ -1269,7 +1394,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();
|
||||
}
|
||||
|
|
396
plugins/calendar/drivers/kolab/kolab_user_calendar.php
Normal file
|
@ -0,0 +1,396 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Kolab calendar storage class simulating a virtual user calendar
|
||||
*
|
||||
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
||||
*
|
||||
* Copyright (C) 2014, Kolab Systems AG <contact@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 $subscriptions = false;
|
||||
|
||||
protected $userdata = array();
|
||||
protected $timeindex = 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_folder_user($this->userdata['kolabtargetfolder'], '', $this->userdata);
|
||||
}
|
||||
else { // get user record from LDAP
|
||||
$this->storage = new kolab_storage_folder_user($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();
|
||||
$this->parent = ''; // user calendars are top level
|
||||
|
||||
// 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'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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)
|
||||
*/
|
||||
public function get_namespace()
|
||||
{
|
||||
return 'other 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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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())
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// avoid session race conditions that will loose temporary subscriptions
|
||||
$this->cal->rc->session->nowrite();
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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->api->dir . 'libkolab/js/folderlist.js')) {
|
||||
$this->cal->api->include_script('libkolab/js/folderlist.js');
|
||||
}
|
||||
|
||||
jqueryui::miniColors();
|
||||
}
|
||||
|
||||
|
@ -157,75 +163,157 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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())
|
||||
{
|
||||
$calendars = $this->cal->driver->list_calendars();
|
||||
$html = '';
|
||||
$jsenv = array();
|
||||
$tree = true;
|
||||
$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' . $id, 'class' => $prop['group']),
|
||||
$content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly'])
|
||||
);
|
||||
}
|
||||
|
||||
$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 folder tree
|
||||
*/
|
||||
public function list_tree_html($node, $data, &$jsenv, $attrib)
|
||||
{
|
||||
$out = '';
|
||||
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, $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' => 'rcmlical' . rcube_utils::html_identifier($id),
|
||||
'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
|
||||
),
|
||||
$content);
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build a calendar list item (HTML content and js data)
|
||||
*/
|
||||
public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
|
||||
{
|
||||
// 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'));
|
||||
|
||||
$jsenv[$id] = $prop;
|
||||
}
|
||||
|
||||
$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) : '');
|
||||
|
||||
if ($prop['virtual'])
|
||||
$classes[] = 'virtual';
|
||||
else if ($prop['readonly'])
|
||||
$classes[] = 'readonly';
|
||||
if ($prop['subscribed'])
|
||||
$classes[] = 'subscribed';
|
||||
if ($prop['class'])
|
||||
$classes[] = $prop['class'];
|
||||
|
||||
$content = '';
|
||||
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'] ? '' :
|
||||
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')), ' ') : '') .
|
||||
html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -86,6 +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'] = 'List permanently';
|
||||
|
||||
// agenda view
|
||||
$labels['listrange'] = 'Range to display:';
|
||||
|
|
|
@ -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 {
|
||||
#calendars .treelist li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
#calendarslist li span.handle {
|
||||
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 {
|
||||
#calendars .treelist div:hover > a.subscribed {
|
||||
background-position: 0 -126px;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
#calendars .treelist ul li div.treetoggle {
|
||||
left: 17px !important;
|
||||
}
|
||||
|
||||
#calendars .treelist ul ul li div.treetoggle {
|
||||
left: 33px !important;
|
||||
}
|
||||
|
||||
#calendars .treelist.flat li input {
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
#calendars .treelist ul li div.folder,
|
||||
#calendars .treelist ul li div.calendar {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
#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;
|
||||
border-bottom: 1px solid #bbb;
|
||||
}
|
||||
|
||||
#calendarslist li.selected span {
|
||||
#calendars .treelist li.selected > span.calname {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#calendarslist li.readonly span.handle {
|
||||
background-position: 0 -20px;
|
||||
#calendars .treelist div.readonly span.calname {
|
||||
background-position: right -20px;
|
||||
}
|
||||
|
||||
#calendarslist li.other span.handle {
|
||||
background-position: 0 -38px;
|
||||
}
|
||||
|
||||
#calendarslist li.other.readonly span.handle {
|
||||
background-position: 0 -56px;
|
||||
}
|
||||
|
||||
#calendarslist li.shared span.handle {
|
||||
background-position: 0 -74px;
|
||||
}
|
||||
|
||||
#calendarslist li.shared.readonly span.handle {
|
||||
background-position: 0 -92px;
|
||||
#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%;
|
||||
|
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -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=" " />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -164,45 +164,86 @@ pre {
|
|||
right: 0;
|
||||
}
|
||||
|
||||
#calendarslist li {
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
padding: 6px 8px 2px;
|
||||
display: block;
|
||||
#calendars .boxtitle {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#calendarslist li.virtual {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
#calendarslist li label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#calendarslist li span.calname {
|
||||
display: block;
|
||||
#calendars .boxtitle a.iconbutton.search {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 26px;
|
||||
right: 24px;
|
||||
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;
|
||||
}
|
||||
|
||||
#calendars .treelist li {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#calendars .treelist li div.folder,
|
||||
#calendars .treelist li div.calendar {
|
||||
position: relative;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
#calendars .treelist li div.virtual {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
|
||||
#calendars .treelist li span.calname {
|
||||
display: block;
|
||||
padding: 0px 30px 2px 2px;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 38px;
|
||||
right: 40px;
|
||||
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 span.handle {
|
||||
#calendars .treelist li div.virtual > span.calname {
|
||||
color: #aaa;
|
||||
top: 4px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
#calendars .treelist.flat li span.calname {
|
||||
left: 24px;
|
||||
right: 22px;
|
||||
}
|
||||
|
||||
#calendars .treelist 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);
|
||||
|
@ -210,43 +251,102 @@ pre {
|
|||
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#calendarslist li input {
|
||||
#calendars .treelist li a.subscribed {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 5px;
|
||||
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;
|
||||
}
|
||||
|
||||
#calendarslist li.selected {
|
||||
#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;
|
||||
left: 18px;
|
||||
}
|
||||
|
||||
#calendars .treelist li div.treetoggle {
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
#calendars .treelist li.virtual div.treetoggle {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
#calendars .treelist.flat li input {
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
#calendars .treelist ul li div.folder,
|
||||
#calendars .treelist ul li div.calendar {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
#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: #c7e3ef;
|
||||
}
|
||||
|
||||
#calendarslist li.selected span.calname {
|
||||
#calendars .treelist li.selected > span.calname {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#calendarslist li.readonly span.calname {
|
||||
#calendars .treelist div.readonly span.calname {
|
||||
background-position: right -20px;
|
||||
}
|
||||
|
||||
#calendarslist li.other span.calname {
|
||||
#calendars .treelist li.user > div > span.calname {
|
||||
background-position: right -38px;
|
||||
}
|
||||
|
||||
#calendarslist li.other.readonly span.calname {
|
||||
/*
|
||||
#calendars .treelist div.user.readonly span.calname {
|
||||
background-position: right -56px;
|
||||
}
|
||||
|
||||
#calendarslist li.shared span.calname {
|
||||
#calendars .treelist div.shared span.calname {
|
||||
background-position: right -74px;
|
||||
}
|
||||
|
||||
#calendarslist li.shared.readonly span.calname {
|
||||
#calendars .treelist div.shared.readonly span.calname {
|
||||
background-position: right -92px;
|
||||
}
|
||||
*/
|
||||
|
||||
#calendarslist li.virtual span.calname {
|
||||
color: #aaa;
|
||||
top: 2px;
|
||||
#calendars .searchresults {
|
||||
background: #b0ccd7;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#calendars .searchresults .boxtitle {
|
||||
background: none;
|
||||
padding: 2px 8px 2px 8px;
|
||||
}
|
||||
|
||||
#calendars .searchresults .listing li {
|
||||
background-color: #c7e3ef;
|
||||
}
|
||||
|
||||
#calfeedurl,
|
||||
|
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.3 KiB |
|
@ -22,16 +22,24 @@
|
|||
<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' />" />
|
||||
<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="⚙" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="calendarsidebartoggle"></div>
|
||||
|
||||
<div id="quicksearchbar">
|
||||
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
|
||||
|
@ -226,30 +234,153 @@ 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){
|
||||
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();
|
||||
}
|
||||
else {
|
||||
$('#calendarlistsearch-reset').click();
|
||||
}
|
||||
// TODO: save state in localStorage
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -213,13 +213,13 @@ 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
|
||||
* @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();
|
||||
|
@ -296,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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -31,3 +35,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');
|
||||
|
||||
|
||||
|
|
225
plugins/libkolab/js/folderlist.js
Normal file
|
@ -0,0 +1,225 @@
|
|||
/**
|
||||
* Kolab groupware folders treelist widget
|
||||
*
|
||||
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
||||
*
|
||||
* @licstart The following is the entire license notice for the
|
||||
* JavaScript code in this file.
|
||||
*
|
||||
* Copyright (C) 2014, Kolab Systems AG <contact@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 search_messagebox;
|
||||
|
||||
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(p.search_title ? '<h2 class="boxtitle">' + p.search_title + '</h2>' : '')
|
||||
.insertAfter(me.container);
|
||||
|
||||
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
|
||||
.appendTo(search_results_container)
|
||||
.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// add results to list
|
||||
for (var prop, item, i=0; i < results.length; i++) {
|
||||
prop = results[i];
|
||||
item = $(prop.html);
|
||||
search_results[prop.id] = prop;
|
||||
search_results_widget.insert({
|
||||
id: prop.id,
|
||||
classes: [ prop.group || '' ],
|
||||
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;
|
||||
item.find('a.subscribed').hide();
|
||||
}
|
||||
}
|
||||
|
||||
search_results_container.show();
|
||||
}
|
||||
}
|
||||
|
||||
// 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: [ prop.group || '' ],
|
||||
virtual: prop.virtual,
|
||||
html: dom_node,
|
||||
}, parent_id, prop.group);
|
||||
}
|
||||
|
||||
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
|
||||
if (search_results_widget) {
|
||||
search_results_container.hide();
|
||||
search_results_widget.reset();
|
||||
}
|
||||
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,
|
||||
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,
|
||||
whendone: function(e){ listsearch_request = null; }
|
||||
});
|
||||
|
||||
listsearch_request = { id:reqid, query:search.query };
|
||||
}
|
||||
else if (!search.query && listsearch_request) {
|
||||
rcmail.multi_thread_request_abort(listsearch_request.id);
|
||||
listsearch_request = null;
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
kolab_folderlist.prototype = rcube_treelist_widget.prototype;
|
|
@ -7,7 +7,7 @@
|
|||
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
||||
* @author Aleksander Machniak <machniak@kolabsys.com>
|
||||
*
|
||||
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
|
||||
* Copyright (C) 2012-2014, Kolab Systems AG <contact@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,12 +37,16 @@ class kolab_storage
|
|||
|
||||
public static $version = '3.0';
|
||||
public static $last_error;
|
||||
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;
|
||||
private static $ldap;
|
||||
|
||||
// Default folder names
|
||||
private static $default_folders = array(
|
||||
|
@ -100,6 +104,41 @@ class kolab_storage
|
|||
return self::$ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes LDAP object to resolve Kolab users
|
||||
*/
|
||||
public static function ldap()
|
||||
{
|
||||
if (self::$ldap) {
|
||||
return self::$ldap;
|
||||
}
|
||||
|
||||
self::setup();
|
||||
|
||||
$config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook'));
|
||||
|
||||
if (!is_array($config)) {
|
||||
$ldap_config = (array)self::$config->get('ldap_public');
|
||||
$config = $ldap_config[$config];
|
||||
}
|
||||
|
||||
if (empty($config)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// overwrite filter option
|
||||
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
|
||||
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
|
||||
|
@ -226,13 +265,57 @@ 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));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 '';
|
||||
}
|
||||
|
||||
|
||||
|
@ -463,7 +546,7 @@ class kolab_storage
|
|||
$folder = substr($folder, $pos+1);
|
||||
}
|
||||
else {
|
||||
$prefix = $folder;
|
||||
$prefix = '('.$folder.')';
|
||||
$folder = '';
|
||||
}
|
||||
|
||||
|
@ -665,18 +748,29 @@ 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 (self::$with_tempsubs && 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;
|
||||
$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();
|
||||
|
@ -696,9 +790,14 @@ class kolab_storage
|
|||
// Get folders list
|
||||
if ($subscribed) {
|
||||
$folders = self::$imap->list_folders_subscribed($root, $mbox);
|
||||
|
||||
// add temporarily 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 (?)
|
||||
|
@ -708,6 +807,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)) {
|
||||
|
@ -721,6 +825,71 @@ 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
|
||||
*
|
||||
* @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
|
||||
|
@ -742,7 +911,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];
|
||||
}
|
||||
|
@ -756,43 +925,65 @@ 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 = null)
|
||||
{
|
||||
$_folders = array();
|
||||
$existing = array_map(function($folder){ return $folder->get_name(); }, $folders);
|
||||
$delim = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
|
||||
$delim = self::$imap->get_hierarchy_delimiter();
|
||||
$other_ns = rtrim(self::namespace_root('other'), $delim);
|
||||
$tree = new kolab_storage_folder_virtual('', '<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))) {
|
||||
$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;
|
||||
}
|
||||
|
||||
while (count($path) >= $depth && ($parent = join($delim, $path))) {
|
||||
array_pop($path);
|
||||
$parent_parent = join($delim, $path);
|
||||
if (!$refs[$parent]) {
|
||||
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_folder_user($parent, $parent_parent);
|
||||
}
|
||||
else {
|
||||
$name = kolab_storage::object_name($parent, $folder->get_namespace());
|
||||
$refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
|
||||
}
|
||||
$parents[] = $refs[$parent];
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
@ -814,13 +1005,55 @@ 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) &&
|
||||
($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
|
||||
$folderdata[$folder] = $metadata;
|
||||
$folderdata += $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
|
||||
$folderdata += $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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];
|
||||
}
|
||||
|
||||
|
||||
|
@ -851,6 +1084,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)) {
|
||||
|
@ -892,17 +1130,21 @@ 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::$with_tempsubs = false;
|
||||
self::$subscriptions = self::$imap->list_folders_subscribed();
|
||||
self::$with_tempsubs = true;
|
||||
}
|
||||
|
||||
return in_array($folder, self::$subscriptions);
|
||||
return in_array($folder, self::$subscriptions) ||
|
||||
($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
|
||||
}
|
||||
|
||||
|
||||
|
@ -910,14 +1152,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;
|
||||
}
|
||||
|
@ -930,14 +1183,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;
|
||||
}
|
||||
|
@ -970,6 +1231,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);
|
||||
}
|
||||
|
||||
|
@ -983,6 +1246,9 @@ class kolab_storage
|
|||
*/
|
||||
public static function folder_deactivate($folder)
|
||||
{
|
||||
// remove from temp subscriptions, really?
|
||||
self::folder_unsubscribe($folder, true);
|
||||
|
||||
return self::set_state($folder, false);
|
||||
}
|
||||
|
||||
|
@ -1006,7 +1272,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, '**');
|
||||
|
@ -1063,7 +1331,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;
|
||||
|
@ -1141,6 +1409,116 @@ 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 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, &$count = 0)
|
||||
{
|
||||
// requires a working LDAP setup
|
||||
if (!self::ldap()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// search users using the configured attributes
|
||||
$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');
|
||||
$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]);
|
||||
$user['kolabtargetfolder'] = $root . $localpart;
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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())
|
||||
{
|
||||
self::setup();
|
||||
|
||||
$folders = array();
|
||||
|
||||
// use localpart of user attribute as root for folder listing
|
||||
$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]);
|
||||
|
||||
$delimiter = self::$imap->get_hierarchy_delimiter();
|
||||
$other_ns = self::namespace_root('other');
|
||||
$folders = self::list_folders($other_ns . $mbox . $delimiter, '*', $type, $subscribed, $folderdata);
|
||||
}
|
||||
|
||||
return $folders;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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($type, $subscribed)
|
||||
{
|
||||
$folders = $folderdata = array();
|
||||
|
||||
if (self::setup()) {
|
||||
$delimiter = self::$imap->get_hierarchy_delimiter();
|
||||
$other_ns = rtrim(self::namespace_root('other'), $delimiter);
|
||||
$path_len = count(explode($delimiter, $other_ns));
|
||||
|
||||
foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
|
||||
if ($foldername == 'INBOX') // skip INBOX which is added by default
|
||||
continue;
|
||||
|
||||
$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]) {
|
||||
$folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $folders;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for user_delete plugin hooks
|
||||
*
|
||||
|
@ -1155,32 +1533,3 @@ class kolab_storage
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $virtual = true;
|
||||
|
||||
public function __construct($realname, $name, $ns)
|
||||
{
|
||||
$this->id = kolab_storage::folder_id($realname);
|
||||
$this->name = $name;
|
||||
$this->namespace = $ns;
|
||||
}
|
||||
|
||||
public function get_namespace()
|
||||
{
|
||||
return $this->namespace;
|
||||
}
|
||||
|
||||
public function get_name()
|
||||
{
|
||||
// this is already kolab_storage::object_name() result
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,26 +22,8 @@
|
|||
* 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
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
|
@ -49,11 +31,6 @@ class kolab_storage_folder
|
|||
public $cache;
|
||||
|
||||
private $type_annotation;
|
||||
private $namespace;
|
||||
private $imap;
|
||||
private $info;
|
||||
private $idata;
|
||||
private $owner;
|
||||
private $resource_uri;
|
||||
|
||||
|
||||
|
@ -62,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);
|
||||
}
|
||||
|
@ -83,6 +60,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)
|
||||
|
@ -92,148 +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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
298
plugins/libkolab/lib/kolab_storage_folder_api.php
Normal file
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Abstract interface class for Kolab storage IMAP folder objects
|
||||
*
|
||||
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
||||
*
|
||||
* Copyright (C) 2014, Kolab Systems AG <contact@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->get_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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
|
101
plugins/libkolab/lib/kolab_storage_folder_user.php
Normal file
|
@ -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@kolabsys.com>
|
||||
*
|
||||
* Copyright (C) 2014, Kolab Systems AG <contact@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);
|
||||
}
|
||||
|
||||
}
|
59
plugins/libkolab/lib/kolab_storage_folder_virtual.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Helper class that represents a virtual IMAP folder
|
||||
* with a subset of the kolab_storage_folder API.
|
||||
*
|
||||
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
||||
*
|
||||
* Copyright (C) 2014, Kolab Systems AG <contact@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()
|
||||
{
|
||||
return $this->displayname ?: parent::get_name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -201,6 +201,18 @@ class tasklist_database_driver extends tasklist_driver
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
|
||||
// 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);
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$utf7name = $folder->name;
|
||||
$tasklist = $this->folder_props($folder, $delim, $prefs);
|
||||
|
||||
$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);
|
||||
|
||||
$fullname = $folder->get_name();
|
||||
$listname = kolab_storage::folder_displayname($fullname, $listnames);
|
||||
|
||||
// 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,
|
||||
'listname' => $listname,
|
||||
'virtual' => true,
|
||||
'editable' => false,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
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 = 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->default ? 'default' : $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()
|
||||
public function get_lists(&$tree = null)
|
||||
{
|
||||
// attempt to create a default list for this user
|
||||
if (empty($this->lists)) {
|
||||
if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true)))
|
||||
$prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true);
|
||||
if ($this->create_list($prop))
|
||||
$this->_read_lists(true);
|
||||
}
|
||||
|
||||
return $this->lists;
|
||||
$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 (!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) {
|
||||
$list_id = $folder->id; #kolab_storage::folder_id($folder->name);
|
||||
$imap_path = explode($delim, $folder->name);
|
||||
|
||||
// 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 = $folder->get_foldername();
|
||||
|
||||
// special handling for virtual folders
|
||||
if ($folder instanceof kolab_storage_folder_user) {
|
||||
$lists[$list_id] = array(
|
||||
'id' => $list_id,
|
||||
'name' => $folder->get_name(),
|
||||
'listname' => $listname,
|
||||
'title' => $folder->get_owner(),
|
||||
'virtual' => true,
|
||||
'editable' => false,
|
||||
'group' => 'other virtual',
|
||||
'class' => 'user',
|
||||
'parent' => $parent_id,
|
||||
);
|
||||
}
|
||||
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 {
|
||||
if (!$this->lists[$list_id]) {
|
||||
$this->lists[$list_id] = $this->folder_props($folder, $delim, $prefs);
|
||||
$this->folders[$list_id] = $folder;
|
||||
}
|
||||
$this->lists[$list_id]['parent'] = $parent_id;
|
||||
$lists[$list_id] = $this->lists[$list_id];
|
||||
}
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected function get_folder($id)
|
||||
{
|
||||
// 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->folders[$id];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new list assigned to the current user
|
||||
*
|
||||
|
@ -197,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;
|
||||
}
|
||||
|
@ -215,7 +307,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 +346,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 +371,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
|
||||
|
@ -283,6 +381,63 @@ class tasklist_kolab_driver extends tasklist_driver
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@ -303,7 +458,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);
|
||||
|
||||
|
@ -324,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;
|
||||
}
|
||||
|
||||
|
@ -367,7 +527,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;
|
||||
|
@ -378,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;
|
||||
}
|
||||
|
||||
|
@ -391,11 +556,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 +589,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 +649,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;
|
||||
|
@ -757,11 +922,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;
|
||||
|
||||
|
@ -810,11 +975,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);
|
||||
}
|
||||
|
||||
|
@ -832,7 +997,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']);
|
||||
|
@ -893,7 +1058,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);
|
||||
}
|
||||
|
||||
|
@ -906,7 +1071,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 = '';
|
||||
|
|
|
@ -125,6 +125,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
|
||||
*
|
||||
|
|
|
@ -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)';
|
||||
|
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.9 KiB |
|
@ -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: -1px 40px 0 0;
|
||||
padding: 0;
|
||||
|
@ -231,32 +257,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;
|
||||
}
|
||||
|
||||
#tasklists li label {
|
||||
#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;
|
||||
}
|
||||
|
||||
#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;
|
||||
|
@ -264,8 +306,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;
|
||||
|
@ -273,52 +318,90 @@ body.attachmentwin #topnav .topright {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tasklists li:hover span.handle {
|
||||
#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;
|
||||
}
|
||||
|
||||
#tasklistsbox .treelist div:hover > a.subscribed {
|
||||
background-position: -2px -215px;
|
||||
}
|
||||
|
||||
#tasklistsbox .treelist div.subscribed a.subscribed {
|
||||
background-position: -20px -215px;
|
||||
}
|
||||
|
||||
#tasklistsbox .treelist li div:hover > span.quickview {
|
||||
background-position: -20px -101px;
|
||||
}
|
||||
|
||||
#tasklists li.focusview span.handle {
|
||||
#tasklistsbox .treelist li div.focusview > span.quickview {
|
||||
background-position: -2px -101px;
|
||||
}
|
||||
|
||||
#tasklists li.selected span.listname {
|
||||
#tasklistsbox .searchresults .treelist li span.quickview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tasklistsbox .treelist li.selected > div > span.listname {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#tasklists li.readonly span.listname {
|
||||
#tasklistsbox .treelist .readonly > span.listname {
|
||||
background-position: right -142px;
|
||||
}
|
||||
|
||||
#tasklists li.other span.listname {
|
||||
#tasklistsbox .treelist .user > span.listname {
|
||||
background-position: right -160px;
|
||||
}
|
||||
|
||||
#tasklists li.other.readonly span.listname {
|
||||
background-position: right -178px;
|
||||
}
|
||||
|
||||
#tasklists li.shared span.listname {
|
||||
background-position: right -196px;
|
||||
}
|
||||
|
||||
#tasklists li.shared.readonly span.listname {
|
||||
background-position: right -214px;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
#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 {
|
||||
|
|
|
@ -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="⚙" />
|
||||
|
@ -175,6 +184,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>
|
||||
|
|
|
@ -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
|
||||
|
@ -674,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;
|
||||
}
|
||||
|
||||
|
@ -993,7 +1057,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,
|
||||
|
@ -1132,7 +1196,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');
|
||||
|
@ -1141,6 +1205,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');
|
||||
|
@ -1193,22 +1258,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({
|
||||
|
@ -1775,15 +1842,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];
|
||||
|
@ -1804,13 +1868,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');
|
||||
|
@ -1822,7 +1888,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(''));
|
||||
|
@ -1832,10 +1898,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() });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1982,7 +2047,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
|
||||
|
@ -2004,13 +2069,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();
|
||||
}
|
||||
|
@ -2019,7 +2086,7 @@ function rcube_tasklist_ui(settings)
|
|||
list_tasks(null);
|
||||
|
||||
if (focusview) {
|
||||
$(rcmail.get_folder_li(focusview, 'rcmlitasklist')).addClass('focusview');
|
||||
li.addClass('focusview');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -598,6 +608,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;
|
||||
}
|
||||
|
@ -620,6 +635,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)
|
||||
|
@ -876,6 +915,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');
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
$lists = $this->plugin->driver->get_lists();
|
||||
$tree = true;
|
||||
$jsenv = array();
|
||||
$lists = $this->plugin->driver->get_lists($tree);
|
||||
|
||||
$li = '';
|
||||
foreach ((array)$lists as $id => $prop) {
|
||||
if ($attrib['activeonly'] && !$prop['active'])
|
||||
continue;
|
||||
// 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)
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
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) : '');
|
||||
|
||||
return html::tag('ul', $attrib, $li, html::$common_attrib);
|
||||
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 '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|