Page MenuHomePhorge

No OneTemporary

Size
58 KB
Referenced Files
None
Subscribers
None
diff --git a/plugins/kolab_addressbook/kolab_addressbook.js b/plugins/kolab_addressbook/kolab_addressbook.js
new file mode 100644
index 00000000..dbb35004
--- /dev/null
+++ b/plugins/kolab_addressbook/kolab_addressbook.js
@@ -0,0 +1,256 @@
+if (window.rcmail) {
+ rcmail.addEventListener('init', function() {
+ rcmail.set_book_actions();
+ if (rcmail.gui_objects.editform && rcmail.env.action.match(/^plugin\.book/)) {
+ rcmail.enable_command('book-save', true);
+ }
+ });
+ rcmail.addEventListener('listupdate', function() {
+ rcmail.set_book_actions();
+ });
+}
+
+// (De-)activates address book management commands
+rcube_webmail.prototype.set_book_actions = function()
+{
+ var source = this.env.source,
+ sources = this.env.address_sources;
+
+ this.enable_command('book-create', true);
+ this.enable_command('book-edit', 'book-delete', source && sources[source] && sources[source].kolab && sources[source].editable);
+};
+
+rcube_webmail.prototype.book_create = function()
+{
+ this.book_show_contentframe('create');
+};
+
+rcube_webmail.prototype.book_edit = function()
+{
+ this.book_show_contentframe('edit');
+};
+
+rcube_webmail.prototype.book_delete = function()
+{
+ if (this.env.source != '' && confirm(this.get_label('kolab_addressbook.bookdeleteconfirm'))) {
+ var lock = this.set_busy(true, 'kolab_addressbook.bookdeleting');
+ this.http_request('plugin.book', '_act=delete&_source='+urlencode(this.book_realname()), lock);
+ }
+};
+
+// displays page with book edit/create form
+rcube_webmail.prototype.book_show_contentframe = function(action, framed)
+{
+ var add_url = '', target = window;
+
+ // unselect contact
+ this.contact_list.clear_selection();
+ this.enable_command('edit', 'delete', 'compose', false);
+
+ if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
+ add_url = '&_framed=1';
+ target = window.frames[this.env.contentframe];
+ this.show_contentframe(true);
+ }
+ else if (framed)
+ return false;
+
+ if (action) {
+ this.set_busy(true);
+ this.location_href(this.env.comm_path+'&_action=plugin.book&_act='+action
+ +'&_source='+urlencode(this.book_realname())
+ +add_url, target);
+ }
+
+ return true;
+};
+
+// submits book create/update form
+rcube_webmail.prototype.book_save = function()
+{
+ var form = this.gui_objects.editform,
+ input = $("input[name='_name']", form)
+
+ if (input.length && input.val() == '') {
+ alert(this.get_label('kolab_addressbook.nobooknamewarning'));
+ input.focus();
+ return;
+ }
+
+ input = this.display_message(this.get_label('kolab_addressbook.booksaving'), 'loading');
+ $('<input type="hidden" name="_unlock" />').val(input).appendTo(form);
+
+ form.submit();
+};
+
+// action executed after book delete
+rcube_webmail.prototype.book_delete_done = function(id)
+{
+ var n, g, li = this.get_folder_li(id), groups = this.env.contactgroups;
+
+ // remove folder and its groups rows
+ for (n in groups)
+ if (groups[n].source == id && (g = this.get_folder_li(n))) {
+ $(g).remove();
+ delete this.env.contactgroups[n];
+ }
+ $(li).remove();
+
+ delete this.env.address_sources[id];
+ delete this.env.contactfolders[id];
+};
+
+// action executed after book create/update
+rcube_webmail.prototype.book_update = function(data, old)
+{
+ var n, i, id, len, row, refrow, olddata, name = '', realname = '', sources, level,
+ folders = [], class_name = 'addressbook',
+ list = this.gui_objects.folderlist,
+ groups = this.env.contactgroups;
+
+ this.env.contactfolders[data.id] = this.env.address_sources[data.id] = data;
+ this.show_contentframe(false);
+
+ // update
+ if (old && old != data.id) {
+ olddata = this.env.address_sources[old];
+ delete this.env.address_sources[old];
+ delete this.env.contactfolders[old];
+
+ // update source ID in groups
+ for (n in groups)
+ if (groups[n].source == old)
+ this.env.contactgroups[n].source = data.id;
+
+ refrow = $('#rcmli'+old);
+ }
+ // create
+ else if (!old) {
+ refrow = $('li', list).get(0);
+
+ // this shouldn't happen
+ if (!refrow)
+ this.redirect(this.get_task_url('addressbook'));
+ }
+
+ if (!refrow)
+ return;
+
+ sources = this.env.address_sources;
+
+ // clone a table row if there are existing rows
+ row = $(refrow).clone();
+
+ // set row attributes
+ if (data.readonly)
+ class_name += ' readonly';
+ if (data.class)
+ class_name += ' '+data.class;
+ // updated currently selected book
+ if (this.env.source != '' && this.env.source == old) {
+ class_name += ' selected';
+ this.env.source = data.id;
+ }
+
+ row.attr({id: 'rcmli'+data.id, 'class': class_name});
+ $('a', row).html(data.name).attr({onclick: '', rel: data.id, href: '#'})
+ .click({id: data.id}, function(e) { return rcmail.command('list', e.data.id, this); });
+
+ // sort kolab folders, to put the new one in order
+ for (n in sources)
+ if (sources[n].kolab && (name = sources[n].realname))
+ folders.push(name);
+ folders.sort();
+
+ // find current id
+ for (n=0, len=folders.length; n<len; n++)
+ if (folders[n] == data.realname)
+ break;
+
+ // add row
+ if (n && n < len) {
+ // find the row before
+ name = folders[n-1];
+ for (n in sources)
+ if (sources[n].realname && sources[n].realname == name) {
+ row.insertAfter('#rcmli'+n);
+ break;
+ }
+ }
+ else if (olddata) {
+ row.insertBefore(refrow);
+ }
+ else {
+ row.appendTo(list);
+ }
+
+ if (olddata) {
+ // remove old row (just after the new row has been inserted)
+ refrow.remove();
+ old += '-';
+ level = olddata.realname.split(this.env.delimiter).length - data.realname.split(this.env.delimiter).length;
+ // update (realname and ID of) subfolders
+ for (n in sources) {
+ if (n.indexOf(old) == 0) {
+ // new ID
+ id = data.id + '-' + n.substr(old.length);
+ name = sources[n].name;
+ realname = data.realname + sources[n].realname.substr(olddata.realname.length);
+
+ // update display name
+ if (level > 0) {
+ for (i=level; i>0; i--)
+ name = name.replace(/^&nbsp;&nbsp;/, '');
+ }
+ else if (level < 0) {
+ for (i=level; i<0; i++)
+ name = '&nbsp;&nbsp;' + name;
+ }
+
+ // update existing row
+ refrow = $('#rcmli'+n);
+ refrow.remove().attr({id: 'rcmli'+id});
+ $('a', refrow).html(name).attr({onclick: '', rel: id, href: '#'})
+ .click({id: id}, function(e) { return rcmail.command('list', e.data.id, this); });
+
+ // move the row to the new place
+ refrow.insertAfter(row);
+ row = refrow;
+
+ // update list data
+ sources[n].id = id;
+ sources[n].name = name;
+ sources[n].realname = realname;
+ this.env.address_sources[id] = this.env.contactfolders[id] = sources[n];
+ delete this.env.address_sources[n];
+ delete this.env.contactfolders[n];
+
+ // update groups
+ for (i in groups) {
+ if (groups[i].source == n) {
+ // update existing row
+ refrow = $('#rcmli'+i);
+ refrow.remove().attr({id: 'rcmliG'+id+groups[i].id});
+ $('a', refrow).attr('onclick', '')
+ .click({source: id, id: groups[i].id}, function(e) {
+ return rcmail.command('listgroup', {'source': e.data.source, 'id': e.data.id}, this); });
+ refrow.insertAfter(row);
+ row = refrow;
+
+ // update group data
+ groups[i].source = id;
+ this.env.contactgroups['G'+id+groups[i].id] = groups[i];
+ delete this.env.contactgroups[i];
+ }
+ }
+ }
+ }
+ }
+};
+
+// returns real IMAP folder name
+rcube_webmail.prototype.book_realname = function()
+{
+ var source = this.env.source, sources = this.env.address_sources;
+ return source != '' && sources[source] && sources[source].realname ? sources[source].realname : '';
+};
diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
new file mode 100644
index 00000000..6bac21b1
--- /dev/null
+++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
@@ -0,0 +1,273 @@
+<?php
+
+/**
+ * Kolab address book UI
+ *
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2011, Kolab Systems AG
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+class kolab_addressbook_ui
+{
+ private $plugin;
+ private $rc;
+
+ /**
+ * Class constructor
+ *
+ * @param kolab_addressbook $plugin Plugin object
+ */
+ public function __construct($plugin)
+ {
+ $this->rc = rcmail::get_instance();
+ $this->plugin = $plugin;
+
+ $this->init_ui();
+ }
+
+ /**
+ * Adds folders management functionality to Addressbook UI
+ */
+ private function init_ui()
+ {
+ if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action)) {
+ return;
+ }
+
+ // Include script
+ $this->plugin->include_script('kolab_addressbook.js');
+
+ if (empty($this->rc->action)) {
+ // Include stylesheet (for directorylist)
+ $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css');
+
+ // Add actions on address books
+ $options = array('book-create', 'book-edit', 'book-delete');
+ $idx = 0;
+
+ foreach ($options as $command) {
+ $content = html::tag('li', $idx ? null : array('class' => 'separator_above'),
+ $this->plugin->api->output->button(array(
+ 'label' => 'kolab_addressbook.'.str_replace('-', '', $command),
+ 'domain' => $this->ID,
+ 'classact' => 'active',
+ 'command' => $command
+ )));
+ $this->plugin->api->add_content($content, 'groupoptions');
+ $idx++;
+ }
+
+ $this->rc->output->add_label('kolab_addressbook.bookdeleteconfirm',
+ 'kolab_addressbook.bookdeleting');
+ }
+ // book create/edit form
+ else {
+ $this->rc->output->add_label('kolab_addressbook.nobooknamewarning',
+ 'kolab_addressbook.booksaving');
+ }
+ }
+
+
+ /**
+ * Handler for address book create/edit action
+ */
+ public function book_edit()
+ {
+ $this->rc->output->add_handler('bookdetails', array($this, 'book_form'));
+ $this->rc->output->send('kolab_addressbook.bookedit');
+ }
+
+
+ /**
+ * Handler for 'bookdetails' object returning form content for book create/edit
+ *
+ * @param array $attr Object attributes
+ *
+ * @return string HTML output
+ */
+ public function book_form($attrib)
+ {
+ $action = trim(get_input_value('_act', RCUBE_INPUT_GPC));
+ $folder = trim(get_input_value('_source', RCUBE_INPUT_GPC, true)); // UTF8
+ $name = trim(get_input_value('_name', RCUBE_INPUT_GPC, true)); // UTF8
+ $old = trim(get_input_value('_oldname', RCUBE_INPUT_GPC, true)); // UTF7-IMAP
+ $path_imap = trim(get_input_value('_parent', RCUBE_INPUT_GPC, true)); // UTF7-IMAP
+
+ $hidden_fields[] = array('name' => '_source', 'value' => $folder);
+
+ $folder = rcube_charset_convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP');
+ $delim = $_SESSION['imap_delimiter'];
+ $form = array();
+
+ if ($this->rc->action == 'plugin.book-save') {
+ // save error
+ $path_imap = $folder;
+ $hidden_fields[] = array('name' => '_oldname', 'value' => $old);
+
+ if (strlen($old)) {
+ $this->rc->imap_connect();
+ $options = $this->rc->imap->mailbox_info($old);
+ }
+ }
+ else if ($action == 'edit') {
+ $path_imap = explode($delim, $folder);
+ $name = rcube_charset_convert(array_pop($path_imap), 'UTF7-IMAP');
+ $path_imap = implode($path_imap, $delim);
+
+ $this->rc->imap_connect();
+ $options = $this->rc->imap->mailbox_info($folder);
+
+ $hidden_fields[] = array('name' => '_oldname', 'value' => $folder);
+ }
+ else {
+ $path_imap = $folder;
+ $name = '';
+ }
+
+ // General tab
+ $form['props'] = array(
+ 'name' => $this->rc->gettext('properties'),
+ );
+
+ $foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30));
+ $foldername = $foldername->show($name);
+
+ $form['props']['fieldsets']['location'] = array(
+ 'name' => $this->rc->gettext('location'),
+ 'content' => array(
+ 'name' => array(
+ 'label' => $this->plugin->gettext('bookname'),
+ 'value' => $foldername,
+ ),
+ ),
+ );
+
+ if (strlen($path_imap)) {
+ $path = rcube_charset_convert($path_imap, 'UTF7-IMAP');
+ // @TODO: $options
+ if (!empty($options) && ($options['norename'] || $options['namespace'] != 'personal')) {
+ // prevent user from moving folder
+ $hidden_fields[] = array('name' => '_parent', 'value' => $path_imap);
+ }
+ else {
+ $radio1 = new html_radiobutton(array('name' => '_parent', 'value' => ''));
+ $radio2 = new html_radiobutton(array('name' => '_parent', 'value' => $path_imap));
+
+ $html_path = str_replace($delim, ' &raquo; ', $path);
+
+ $folderpath = $radio1->show($path_imap) . Q(rcube_label('none')) . '&nbsp;'
+ .$radio2->show($path_imap) . Q($html_path);
+
+ $form['props']['fieldsets']['location']['content']['path'] = array(
+ 'label' => $this->plugin->gettext('parentbook'),
+ 'value' => $folderpath,
+ );
+ }
+ }
+
+ // Allow plugins to modify address book form content (e.g. with ACL form)
+ $plugin = $this->rc->plugins->exec_hook('addressbook_form',
+ array('form' => $form, 'options' => $options, 'name' => $folder));
+
+ $form = $plugin['form'];
+
+ // Set form tags and hidden fields
+ list($form_start, $form_end) = $this->get_form_tags($attrib, 'plugin.book-save', null, $hidden_fields);
+
+ unset($attrib['form']);
+
+ // return the complete edit form as table
+ $out = "$form_start\n";
+
+ // Create form output
+ foreach ($form as $tab) {
+ if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
+ $content = '';
+ foreach ($tab['fieldsets'] as $fieldset) {
+ $subcontent = $this->get_form_part($fieldset);
+ if ($subcontent) {
+ $content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n";
+ }
+ }
+ }
+ else {
+ $content = $this->get_form_part($tab);
+ }
+
+ if ($content) {
+ $out .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";
+ }
+ }
+
+ $out .= "\n$form_end";
+
+ return $out;
+ }
+
+
+ private function get_form_part($form)
+ {
+ $content = '';
+
+ if (is_array($form['content']) && !empty($form['content'])) {
+ $table = new html_table(array('cols' => 2));
+ foreach ($form['content'] as $col => $colprop) {
+ $colprop['id'] = '_'.$col;
+ $label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
+
+ $table->add('title', sprintf('<label for="%s">%s</label>', $colprop['id'], Q($label)));
+ $table->add(null, $colprop['value']);
+ }
+ $content = $table->show();
+ }
+ else {
+ $content = $form['content'];
+ }
+
+ return $content;
+ }
+
+
+ private function get_form_tags($attrib, $action, $id = null, $hidden = null)
+ {
+ $form_start = $form_end = '';
+
+ $request_key = $action . (isset($id) ? '.'.$id : '');
+ $form_start = $this->rc->output->request_form(array(
+ 'name' => 'form',
+ 'method' => 'post',
+ 'task' => $this->rc->task,
+ 'action' => $action,
+ 'request' => $request_key,
+ 'noclose' => true,
+ ) + $attrib);
+
+ if (is_array($hidden)) {
+ foreach ($hidden as $field) {
+ $hiddenfield = new html_hiddenfield($field);
+ $form_start .= $hiddenfield->show();
+ }
+ }
+
+ $form_end = !strlen($attrib['form']) ? '</form>' : '';
+
+ $EDIT_FORM = !empty($attrib['form']) ? $attrib['form'] : 'form';
+ $this->rc->output->add_gui_object('editform', $EDIT_FORM);
+
+ return array($form_start, $form_end);
+ }
+
+}
diff --git a/plugins/kolab_addressbook/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
similarity index 88%
rename from plugins/kolab_addressbook/rcube_kolab_contacts.php
rename to plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index af3daa8d..3c578e6d 100644
--- a/plugins/kolab_addressbook/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1,950 +1,1072 @@
<?php
/**
* Backend class for a custom address book
*
* This part of the Roundcube+Kolab integration and connects the
* rcube_addressbook interface with the rcube_kolab wrapper for Kolab_Storage
*
* @author Thomas Bruederli
* @see rcube_addressbook
*/
class rcube_kolab_contacts extends rcube_addressbook
{
public $primary_key = 'ID';
- public $readonly = false;
- public $groups = true;
+ public $readonly = true;
+ public $editable = false;
+ public $groups = false;
public $coltypes = array(
'name' => array('limit' => 1),
'firstname' => array('limit' => 1),
'surname' => array('limit' => 1),
'middlename' => array('limit' => 1),
'prefix' => array('limit' => 1),
'suffix' => array('limit' => 1),
'nickname' => array('limit' => 1),
'jobtitle' => array('limit' => 1),
'organization' => array('limit' => 1),
'department' => array('limit' => 1),
'email' => array('subtypes' => null),
'phone' => array(),
'address' => array('limit' => 2, 'subtypes' => array('home','business')),
'officelocation' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.officelocation', 'category' => 'main'),
'website' => array('limit' => 1, 'subtypes' => null),
'im' => array('limit' => 1, 'subtypes' => null),
'gender' => array('limit' => 1),
'initials' => array('type' => 'text', 'size' => 6, 'limit' => 1,
'label' => 'kolab_addressbook.initials', 'category' => 'personal'),
'birthday' => array('limit' => 1),
'anniversary' => array('limit' => 1),
'profession' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.profession', 'category' => 'personal'),
'manager' => array('limit' => 1),
'assistant' => array('limit' => 1),
'spouse' => array('limit' => 1),
'children' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.children', 'category' => 'personal'),
'pgppublickey' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.pgppublickey'),
'freebusyurl' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.freebusyurl'),
'notes' => array(),
'photo' => array(),
// TODO: define more Kolab-specific fields such as: language, latitude, longitude
);
private $gid;
- private $imap;
- private $kolab;
- private $folder;
private $contactstorage;
private $liststorage;
private $contacts;
private $distlists;
private $groupmembers;
private $id2uid;
private $filter;
private $result;
+ private $namespace;
private $imap_folder = 'INBOX/Contacts';
private $gender_map = array(0 => 'male', 1 => 'female');
private $phonetypemap = array('home' => 'home1', 'work' => 'business1', 'work2' => 'business2', 'workfax' => 'businessfax');
private $addresstypemap = array('work' => 'business');
private $fieldmap = array(
// kolab => roundcube
'full-name' => 'name',
'given-name' => 'firstname',
'middle-names' => 'middlename',
'last-name' => 'surname',
'prefix' => 'prefix',
'suffix' => 'suffix',
'nick-name' => 'nickname',
'organization' => 'organization',
'department' => 'department',
'job-title' => 'jobtitle',
'initials' => 'initials',
'birthday' => 'birthday',
'anniversary' => 'anniversary',
'im-address' => 'im',
'web-page' => 'website',
'office-location' => 'officelocation',
'profession' => 'profession',
'manager-name' => 'manager',
'assistant' => 'assistant',
'spouse-name' => 'spouse',
'children' => 'children',
'body' => 'notes',
'pgp-publickey' => 'pgppublickey',
'free-busy-url' => 'freebusyurl',
);
public function __construct($imap_folder = null)
{
if ($imap_folder)
$this->imap_folder = $imap_folder;
// extend coltypes configuration
$format = rcube_kolab::get_format('contact');
$this->coltypes['phone']['subtypes'] = $format->_phone_types;
$this->coltypes['address']['subtypes'] = $format->_address_types;
// set localized labels for proprietary cols
foreach ($this->coltypes as $col => $prop) {
if (is_string($prop['label']))
$this->coltypes[$col]['label'] = rcube_label($prop['label']);
}
// fetch objects from the given IMAP folder
$this->contactstorage = rcube_kolab::get_storage($this->imap_folder);
$this->liststorage = rcube_kolab::get_storage($this->imap_folder, 'distributionlist');
$this->ready = !PEAR::isError($this->contactstorage) && !PEAR::isError($this->liststorage);
+
+ // Set readonly and editable flags according to folder permissions
+ if ($this->ready) {
+ if ($this->get_owner() == $_SESSION['username']) {
+ $this->editable = true;
+ $this->readonly = false;
+ }
+ else {
+ $acl = $this->contactstorage->_folder->getACL();
+ $acl = $acl[$_SESSION['username']];
+ if (strpos($acl, 'i') !== false)
+ $this->readonly = false;
+ if (strpos($acl, 'a') !== false || strpos($acl, 'x') !== false)
+ $this->editable = true;
+ }
+
+ if (!$this->readonly)
+ $this->groups = true;
+ }
}
/**
* Getter for the address book name to be displayed
*
* @return string Name of this address book
*/
public function get_name()
{
- $folder = rcube_charset_convert($this->imap_folder, 'UTF7-IMAP');
- // @TODO: use namespace prefixes
- return strtr(preg_replace('!^(INBOX|user)/!i', '', $folder), '/', ':');
+ $folder = $this->imap_folder;
+ $namespace = $_SESSION['imap_namespace']; // from rcube_imap class
+ $found = false;
+
+ if (!empty($namespace['shared'])) {
+ foreach ($namespace['shared'] as $ns) {
+ if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+ $prefix = '';
+ $folder = substr($folder, strlen($ns[0]));
+ $delim = $ns[1];
+ $found = true;
+ $this->namespace = 'shared';
+ break;
+ }
+ }
+ }
+ if (!$found && !empty($namespace['other'])) {
+ foreach ($namespace['other'] as $ns) {
+ if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+ // remove namespace prefix
+ $folder = substr($folder, strlen($ns[0]));
+ $delim = $ns[1];
+ // get username
+ $pos = strpos($folder, $delim);
+ $prefix = '('.substr($folder, 0, $pos).') ';
+ $found = true;
+ $this->namespace = 'other';
+ break;
+ }
+ }
+ }
+ if (!$found && !empty($namespace['personal'])) {
+ foreach ($namespace['personal'] as $ns) {
+ if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+ // remove namespace prefix
+ $folder = substr($folder, strlen($ns[0]));
+ $prefix = '';
+ $delim = $ns[1];
+ $found = true;
+ $this->namespace = 'personal';
+ break;
+ }
+ }
+ }
+
+ if (empty($delim))
+ $delim = $_SESSION['imap_delimiter']; // from rcube_imap class
+
+ $folder = rcube_charset_convert($folder, 'UTF7-IMAP');
+ $folder = str_replace($delim, ' &raquo; ', $folder);
+
+ if ($prefix)
+ $folder = $prefix . ' ' . $folder;
+
+ return $folder;
+ }
+
+
+ /**
+ * Getter for the IMAP folder name
+ *
+ * @return string Name of the IMAP folder
+ */
+ public function get_realname()
+ {
+ return $this->imap_folder;
+ }
+
+
+ /**
+ * Getter for the IMAP folder owner
+ *
+ * @return string Name of the folder owner
+ */
+ public function get_owner()
+ {
+ return $this->contactstorage->_folder->getOwner();
+ }
+
+
+ /**
+ * 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 ($this->namespace) {
+ return $this->namespace;
+ }
+
+ $folder = $this->imap_folder;
+ $namespace = $_SESSION['imap_namespace']; // from rcube_imap class
+
+ if (!empty($namespace)) {
+ foreach ($namespace as $nsname => $nsvalue) {
+ if (in_array($nsname, array('personal', 'other', 'shared')) && !empty($nsvalue)) {
+ foreach ($nsvalue as $ns) {
+ if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+ return $this->namespace = $nsname;
+ }
+ }
+ }
+ }
+ }
+
+ return $this->namespace = 'personal';
}
/**
* Setter for the current group
*/
public function set_group($gid)
{
$this->gid = $gid;
}
/**
* Save a search string for future listings
*
* @param mixed Search params to use in listing method, obtained by get_search_set()
*/
public function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
public function get_search_set()
{
return $this->filter;
}
/**
* Reset saved results and search parameters
*/
public function reset()
{
$this->result = null;
$this->filter = null;
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null)
{
$this->_fetch_groups();
$groups = array();
foreach ((array)$this->distlists as $group) {
if (!$search || strstr(strtolower($group['last-name']), strtolower($search)))
$groups[] = array('ID' => $group['ID'], 'name' => $group['last-name']);
}
return $groups;
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records, use negative values for tail
* @return array Indexed list of contact records, each a hash array
*/
public function list_records($cols=null, $subset=0)
{
$this->result = $this->count();
// list member of the selected group
if ($this->gid) {
$seen = array();
$this->result->count = 0;
foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
// skip member that don't match the search filter
if (is_array($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false)
continue;
if ($this->contacts[$member['ID']] && !$seen[$member['ID']]++)
$this->result->count++;
}
$ids = array_keys($seen);
}
else
$ids = is_array($this->filter['ids']) ? $this->filter['ids'] : array_keys($this->contacts);
// fill contact data into the current result set
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, count($ids));
for ($i = $start_row; $i < $last_row; $i++) {
if ($id = $ids[$i])
$this->result->add($this->contacts[$id]);
}
return $this->result;
}
/**
* Search records
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param boolean $strict True for strict (=), False for partial (LIKE) matching
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set List of contact records and 'count' value
*/
public function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
{
$this->_fetch_contacts();
// search by ID
if ($fields == $this->primary_key) {
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id) {
if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
}
return $result;
}
else if ($fields == '*') {
$fields = array_keys($this->coltypes);
}
if (!is_array($fields))
$fields = array($fields);
if (!is_array($required) && !empty($required))
$required = array($required);
// advanced search
if (is_array($value)) {
$advanced = true;
$value = array_map('mb_strtolower', $value);
}
else
$value = mb_strtolower($value);
$scount = count($fields);
// build key name regexp
$regexp = '/^(' . implode($fields, '|') . ')(?:.*)$/';
// save searching conditions
$this->filter = array('fields' => $fields, 'value' => $value, 'strict' => $strict, 'ids' => array());
// search be iterating over all records in memory
foreach ($this->contacts as $id => $contact) {
// check if current contact has required values, otherwise skip it
if ($required) {
foreach ($required as $f)
if (empty($contact[$f]))
continue 2;
}
$found = array();
foreach (preg_grep($regexp, array_keys($contact)) as $col) {
if ($advanced) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
$search = $value[array_search($colname, $fields)];
}
else {
$search = $value;
}
foreach ((array)$contact[$col] as $val) {
// composite field, e.g. address
if (is_array($val)) {
$val = implode($val);
}
$val = mb_strtolower($val);
if (($strict && $val == $search) || (!$strict && strpos($val, $search) !== false)) {
if (!$advanced) {
$this->filter['ids'][] = $id;
break 2;
}
else {
$found[$colname] = true;
}
}
}
}
if (count($found) >= $scount) // && $advanced
$this->filter['ids'][] = $id;
}
// list records (now limited by $this->filter)
return $this->list_records();
}
/**
* Refresh saved search results after data has changed
*/
public function refresh_search()
{
if ($this->filter)
$this->search($this->filter['fields'], $this->filter['value'], $this->filter['strict']);
return $this->get_search_set();
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
public function count()
{
$this->_fetch_contacts();
$this->_fetch_groups();
$count = $this->gid ? count($this->distlists[$this->gid]['member']) : (is_array($this->filter['ids']) ? count($this->filter['ids']) : count($this->contacts));
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return rcube_result_set Current result set or NULL if nothing selected yet
*/
public function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed record identifier(s)
* @param boolean True to return record as associative array, otherwise a result set is returned
* @return mixed Result object with all record fields or False if not found
*/
public function get_record($id, $assoc=false)
{
$this->_fetch_contacts();
if ($this->contacts[$id]) {
$this->result = new rcube_result_set(1);
$this->result->add($this->contacts[$id]);
return $assoc ? $this->contacts[$id] : $this->result;
}
return false;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
* @return array List of assigned groups as ID=>Name pairs
*/
public function get_record_groups($id)
{
$out = array();
$this->_fetch_groups();
foreach ((array)$this->groupmembers[$id] as $gid) {
if ($group = $this->distlists[$gid])
$out[$gid] = $group['last-name'];
}
return $out;
}
/**
* Create a new contact record
*
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean True to check for duplicates first
* @return mixed The created record ID on success, False on error
*/
public function insert($save_data, $check=false)
{
if (!is_array($save_data))
return false;
$insert_id = $existing = false;
// check for existing records by e-mail comparison
if ($check) {
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (($res = $this->search('email', $email, true, false)) && $res->count) {
$existing = true;
break;
}
}
}
if (!$existing) {
// generate new Kolab contact item
$object = $this->_from_rcube_contact($save_data);
$object['uid'] = $this->contactstorage->generateUID();
$saved = $this->contactstorage->save($object);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$contact = $this->_to_rcube_contact($object);
$id = $contact['ID'];
$this->contacts[$id] = $contact;
$this->id2uid[$id] = $object['uid'];
$insert_id = $id;
}
}
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @return boolean True on success, False on error
*/
public function update($id, $save_data)
{
$updated = false;
$this->_fetch_contacts();
if ($this->contacts[$id] && ($uid = $this->id2uid[$id])) {
$old = $this->contactstorage->getObject($uid);
$object = array_merge($old, $this->_from_rcube_contact($save_data));
$saved = $this->contactstorage->save($object, $uid);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$this->contacts[$id] = $this->_to_rcube_contact($object);
$updated = true;
}
}
return $updated;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
*/
public function delete($ids)
{
$this->_fetch_contacts();
$this->_fetch_groups();
if (!is_array($ids))
$ids = explode(',', $ids);
$count = 0;
foreach ($ids as $id) {
if ($uid = $this->id2uid[$id]) {
$deleted = $this->contactstorage->delete($uid);
if (PEAR::isError($deleted)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting a contact object from the Kolab server:" . $deleted->getMessage()),
true, false);
}
else {
// remove from distribution lists
foreach ((array)$this->groupmembers[$id] as $gid)
$this->remove_from_group($gid, $id);
// clear internal cache
unset($this->contacts[$id], $this->id2uid[$id], $this->groupmembers[$id]);
$count++;
}
}
}
return $count;
}
/**
* Remove all records from the database
*/
public function delete_all()
{
if (!PEAR::isError($this->contactstorage->deleteAll())) {
$this->contacts = array();
$this->id2uid = array();
$this->result = null;
}
}
/**
* Close connection to source
* Called on script shutdown
*/
public function close()
{
rcube_kolab::shutdown();
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$this->_fetch_groups();
$result = false;
$list = array(
'uid' => $this->liststorage->generateUID(),
'last-name' => $name,
'member' => array(),
);
$saved = $this->liststorage->save($list);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
return false;
}
else {
$id = md5($list['uid']);
$this->distlists[$record['ID']] = $list;
$result = array('id' => $id, 'name' => $name);
}
return $result;
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($gid)
{
$this->_fetch_groups();
$result = false;
if ($list = $this->distlists[$gid])
$deleted = $this->liststorage->delete($list['uid']);
if (PEAR::isError($deleted)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting distribution-list object from the Kolab server:" . $deleted->getMessage()),
true, false);
}
else
$result = true;
return $result;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @return boolean New name on success, false if no data was changed
*/
function rename_group($gid, $newname)
{
$this->_fetch_groups();
$list = $this->distlists[$gid];
if ($newname != $list['last-name']) {
$list['last-name'] = $newname;
$saved = $this->liststorage->save($list, $list['uid']);
}
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
return false;
}
return $newname;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($gid, $ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$added = 0;
$exists = array();
$this->_fetch_groups();
$this->_fetch_contacts();
$list = $this->distlists[$gid];
foreach ((array)$list['member'] as $i => $member)
$exists[] = $member['ID'];
// substract existing assignments from list
$ids = array_diff($ids, $exists);
foreach ($ids as $contact_id) {
if ($uid = $this->id2uid[$contact_id]) {
$contact = $this->contacts[$contact_id];
foreach ($this->get_col_values('email', $contact, true) as $email) {
$list['member'][] = array(
'uid' => $uid,
'display-name' => $contact['name'],
'smtp-address' => $email,
);
}
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
}
if ($added)
$saved = $this->liststorage->save($list, $list['uid']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list to Kolab server:" . $saved->getMessage()),
true, false);
$added = false;
}
else {
$this->distlists[$gid] = $list;
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($gid, $ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$this->_fetch_groups();
if (!($list = $this->distlists[$gid]))
return false;
$new_member = array();
foreach ((array)$list['member'] as $member) {
if (!in_array($member['ID'], $ids))
$new_member[] = $member;
}
// write distribution list back to server
$list['member'] = $new_member;
$saved = $this->liststorage->save($list, $list['uid']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
// remove group assigments in local cache
foreach ($ids as $id) {
$j = array_search($gid, $this->groupmembers[$id]);
unset($this->groupmembers[$id][$j]);
}
$this->distlists[$gid] = $list;
return true;
}
return false;
}
/**
* Simply fetch all records and store them in private member vars
*/
private function _fetch_contacts()
{
if (!isset($this->contacts)) {
// read contacts
$this->contacts = $this->id2uid = array();
foreach ((array)$this->contactstorage->getObjects() as $record) {
$contact = $this->_to_rcube_contact($record);
$id = $contact['ID'];
$this->contacts[$id] = $contact;
$this->id2uid[$id] = $record['uid'];
}
// sort data arrays according to desired list sorting
uasort($this->contacts, array($this, '_sort_contacts_comp'));
}
}
/**
* Callback function for sorting contacts
*/
private function _sort_contacts_comp($a, $b)
{
return strcasecmp($a['name'], $b['name']);
}
/**
* Read distribution-lists AKA groups from server
*/
private function _fetch_groups()
{
if (!isset($this->distlists)) {
$this->distlists = $this->groupmembers = array();
foreach ((array)$this->liststorage->getObjects() as $record) {
// FIXME: folders without any distribution-list objects return contacts instead ?!
if ($record['__type'] != 'Group')
continue;
$record['ID'] = md5($record['uid']);
foreach ((array)$record['member'] as $i => $member) {
$mid = md5($member['uid']);
$record['member'][$i]['ID'] = $mid;
$this->groupmembers[$mid][] = $record['ID'];
}
$this->distlists[$record['ID']] = $record;
}
}
}
/**
* Map fields from internal Kolab_Format to Roundcube contact format
*/
private function _to_rcube_contact($record)
{
$out = array(
'ID' => md5($record['uid']),
'email' => array(),
'phone' => array(),
);
foreach ($this->fieldmap as $kolab => $rcube) {
if (strlen($record[$kolab]))
$out[$rcube] = $record[$kolab];
}
if (isset($record['gender']))
$out['gender'] = $this->gender_map[$record['gender']];
foreach ((array)$record['email'] as $i => $email)
$out['email'][] = $email['smtp-address'];
if (!$record['email'] && $record['emails'])
$out['email'] = preg_split('/,\s*/', $record['emails']);
foreach ((array)$record['phone'] as $i => $phone)
$out['phone:'.$phone['type']][] = $phone['number'];
if (is_array($record['address'])) {
foreach ($record['address'] as $i => $adr) {
$key = 'address:' . $adr['type'];
$out[$key][] = array(
'street' => $adr['street'],
'locality' => $adr['locality'],
'zipcode' => $adr['postal-code'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
// photo is stored as separate attachment
if ($record['picture'] && ($att = $record['_attachments'][$record['picture']])) {
$out['photo'] = $att['content'] ? $att['content'] : $this->contactstorage->getAttachment($att['key']);
}
// remove empty fields
return array_filter($out);
}
private function _from_rcube_contact($contact)
{
$object = array();
foreach (array_flip($this->fieldmap) as $rcube => $kolab) {
if (isset($contact[$rcube]))
$object[$kolab] = is_array($contact[$rcube]) ? $contact[$rcube][0] : $contact[$rcube];
else if ($values = $this->get_col_values($rcube, $contact, true))
$object[$kolab] = is_array($values) ? $values[0] : $values;
}
// format dates
if ($object['birthday'] && ($date = @strtotime($object['birthday'])))
$object['birthday'] = date('Y-m-d', $date);
if ($object['anniversary'] && ($date = @strtotime($object['anniversary'])))
$object['anniversary'] = date('Y-m-d', $date);
$gendermap = array_flip($this->gender_map);
if (isset($contact['gender']))
$object['gender'] = $gendermap[$contact['gender']];
$emails = $this->get_col_values('email', $contact, true);
$object['emails'] = join(', ', array_filter($emails));
// overwrite 'email' field
$object['email'] = '';
foreach ($this->get_col_values('phone', $contact) as $type => $values) {
if ($this->phonetypemap[$type])
$type = $this->phonetypemap[$type];
foreach ((array)$values as $phone) {
if (!empty($phone)) {
$object['phone-' . $type] = $phone;
$object['phone'][] = array('number' => $phone, 'type' => $type);
}
}
}
foreach ($this->get_col_values('address', $contact) as $type => $values) {
if ($this->addresstypemap[$type])
$type = $this->addresstypemap[$type];
$basekey = 'addr-' . $type . '-';
foreach ((array)$values as $adr) {
// switch type if slot is already taken
if (isset($object[$basekey . 'type'])) {
$type = $type == 'home' ? 'business' : 'home';
$basekey = 'addr-' . $type . '-';
}
if (!isset($object[$basekey . 'type'])) {
$object[$basekey . 'type'] = $type;
$object[$basekey . 'street'] = $adr['street'];
$object[$basekey . 'locality'] = $adr['locality'];
$object[$basekey . 'postal-code'] = $adr['zipcode'];
$object[$basekey . 'region'] = $adr['region'];
$object[$basekey . 'country'] = $adr['country'];
}
else {
$object['address'][] = array(
'type' => $type,
'street' => $adr['street'],
'locality' => $adr['locality'],
'postal-code' => $adr['zipcode'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
}
// save new photo as attachment
if ($contact['photo']) {
$attkey = 'photo.attachment';
$object['_attachments'][$attkey] = array(
'type' => rc_image_content_type($contact['photo']),
'content' => preg_match('![^a-z0-9/=+-]!i', $contact['photo']) ? $contact['photo'] : base64_decode($contact['photo']),
);
$object['picture'] = $attkey;
}
return $object;
}
}
diff --git a/plugins/kolab_addressbook/skins/default/kolab_addressbook.css b/plugins/kolab_addressbook/skins/default/kolab_addressbook.css
new file mode 100644
index 00000000..da3abed2
--- /dev/null
+++ b/plugins/kolab_addressbook/skins/default/kolab_addressbook.css
@@ -0,0 +1,35 @@
+#directorylist li.addressbook.readonly
+{
+ /* don't use 'background' to not reset background color */
+ background-image: url(kolab_folders.png);
+ background-position: 5px 0px;
+ background-repeat: no-repeat;
+}
+
+#directorylist li.addressbook.shared
+{
+ background-image: url(kolab_folders.png);
+ background-position: 5px -54px;
+ background-repeat: no-repeat;
+}
+
+#directorylist li.addressbook.shared.readonly
+{
+ background-image: url(kolab_folders.png);
+ background-position: 5px -72px;
+ background-repeat: no-repeat;
+}
+
+#directorylist li.addressbook.other
+{
+ background-image: url(kolab_folders.png);
+ background-position: 5px -18px;
+ background-repeat: no-repeat;
+}
+
+#directorylist li.addressbook.other.readonly
+{
+ background-image: url(kolab_folders.png);
+ background-position: 5px -36px;
+ background-repeat: no-repeat;
+}
diff --git a/plugins/kolab_addressbook/skins/default/kolab_folders.png b/plugins/kolab_addressbook/skins/default/kolab_folders.png
new file mode 100644
index 00000000..cd6228a7
Binary files /dev/null and b/plugins/kolab_addressbook/skins/default/kolab_folders.png differ
diff --git a/plugins/kolab_addressbook/skins/default/templates/bookedit.html b/plugins/kolab_addressbook/skins/default/templates/bookedit.html
new file mode 100644
index 00000000..717ed8c4
--- /dev/null
+++ b/plugins/kolab_addressbook/skins/default/templates/bookedit.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+<link rel="stylesheet" type="text/css" href="/settings.css" />
+<script type="text/javascript" src="/functions.js"></script>
+</head>
+<body class="iframe">
+
+<div id="folder-title" class="boxtitle"><roundcube:label name="kolab_addressbook.bookproperties" /></div>
+
+<div id="folder-details" class="boxcontent">
+ <roundcube:object name="bookdetails" />
+ <p>
+<!--
+ <roundcube:if condition="!strlen(request:_source)" />
+ <input type="button" value="<roundcube:label name="cancel" />" class="button" onclick="history.back()" />&nbsp;
+ <roundcube:endif />
+-->
+ <roundcube:button command="book-save" type="input" class="button mainaction" label="save" />
+ </p>
+</div>
+<script type="text/javascript">rcube_init_tabs('folder-details > form')</script>
+
+</body>
+</html>

File Metadata

Mime Type
text/x-diff
Expires
Thu, Dec 18, 10:58 AM (51 m, 44 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
418751
Default Alt Text
(58 KB)

Event Timeline