Page MenuHomePhorge

No OneTemporary

diff --git a/plugins/kolab_delegation/kolab_delegation_engine.php b/plugins/kolab_delegation/kolab_delegation_engine.php
index a286e4b2..48c934b2 100644
--- a/plugins/kolab_delegation/kolab_delegation_engine.php
+++ b/plugins/kolab_delegation/kolab_delegation_engine.php
@@ -1,969 +1,969 @@
<?php
/**
* Kolab Delegation Engine
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2012, 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_delegation_engine
{
public $context;
private $rc;
private $ldap;
private $ldap_filter;
private $ldap_delegate_field;
private $ldap_login_field;
private $ldap_name_field;
private $ldap_email_field;
private $ldap_org_field;
private $ldap_dn;
private $cache = array();
private $folder_types = array('mail', 'event', 'task');
private $supported;
const ACL_READ = 1;
const ACL_WRITE = 2;
/**
* Class constructor
*/
public function __construct()
{
$this->rc = rcube::get_instance();
}
/**
* Add delegate
*
* @param string|array $delegate Delegate DN (encoded) or delegate data (result of delegate_get())
* @param array $acl List of folder->right map
*
* @return string On error returns an error label, on success returns null
*/
public function delegate_add($delegate, $acl)
{
if (!is_array($delegate)) {
$delegate = $this->delegate_get($delegate);
}
$dn = $delegate['ID'];
if (empty($delegate) || empty($dn)) {
return 'createerror';
}
$list = $this->list_delegates();
$list = array_keys((array)$list);
$list = array_filter($list);
if (in_array($dn, $list)) {
return 'delegationexisterror';
}
// add delegate to the list
$list[] = $dn;
$list = array_map(array('kolab_ldap', 'dn_decode'), $list);
// update user record
$result = $this->user_update_delegates($list);
// Set ACL on folders
if ($result && !empty($acl)) {
$this->delegate_acl_update($delegate['uid'], $acl);
}
return $result ? null : 'createerror';
}
/**
* Set/Update ACL on delegator's folders
*
* @param string $uid Delegate authentication identifier
* @param array $acl List of folder->right map
* @param bool $update Update (remove) old rights
*
* @return string On error returns an error label, on success returns null
*/
public function delegate_acl_update($uid, $acl, $update = false)
{
$storage = $this->rc->get_storage();
$right_types = $this->right_types();
$folders = $update ? $this->list_folders($uid) : array();
foreach ($acl as $folder_name => $rights) {
- $r = $right_types[$rights];
+ $r = $right_types[$rights] ?? null;
if ($r) {
$storage->set_acl($folder_name, $uid, $r);
}
else {
$storage->delete_acl($folder_name, $uid);
}
if (!empty($folders) && isset($folders[$folder_name])) {
unset($folders[$folder_name]);
}
}
foreach ($folders as $folder_name => $folder) {
- if ($folder['rights']) {
+ if (!empty($folder['rights'])) {
$storage->delete_acl($folder_name, $uid);
}
}
}
/**
* Delete delgate
*
* @param string $dn Delegate DN (encoded)
* @param bool $acl_del Enable ACL deletion on delegator folders
*
* @return string On error returns an error label, on success returns null
*/
public function delegate_delete($dn, $acl_del = false)
{
$delegate = $this->delegate_get($dn);
$list = $this->list_delegates();
$user = $this->user();
if (empty($delegate) || !isset($list[$dn])) {
return 'deleteerror';
}
// remove delegate from the list
unset($list[$dn]);
$list = array_keys($list);
$list = array_map(array('kolab_ldap', 'dn_decode'), $list);
$user[$this->ldap_delegate_field] = $list;
// update user record
$result = $this->user_update_delegates($list);
// remove ACL
if ($result && $acl_del) {
$this->delegate_acl_update($delegate['uid'], array(), true);
}
return $result ? null : 'deleteerror';
}
/**
* Return delegate data
*
* @param string $dn Delegate DN (encoded)
*
* @return array Delegate record (ID, name, uid, imap_uid)
*/
public function delegate_get($dn)
{
// use internal cache so we not query LDAP more than once per request
if (!isset($this->cache[$dn])) {
$ldap = $this->ldap();
if (!$ldap || empty($dn)) {
return array();
}
// Get delegate
$user = $ldap->get_record(kolab_ldap::dn_decode($dn));
if (empty($user)) {
return array();
}
$delegate = $this->parse_ldap_record($user);
$delegate['ID'] = $dn;
$this->cache[$dn] = $delegate;
}
return $this->cache[$dn];
}
/**
* Return delegate data
*
* @param string $login Delegate name (the 'uid' returned in get_users())
*
* @return array Delegate record (ID, name, uid, imap_uid)
*/
public function delegate_get_by_name($login)
{
$ldap = $this->ldap();
if (!$ldap || empty($login)) {
return array();
}
$list = $ldap->dosearch($this->ldap_login_field, $login, 1);
if (count($list) == 1) {
$dn = key($list);
$user = $list[$dn];
return $this->parse_ldap_record($user, $dn);
}
}
/**
* LDAP object getter
*/
private function ldap()
{
if ($this->ldap !== null) {
return $this->ldap;
}
$this->ldap = kolab_storage::ldap('kolab_delegation_addressbook');
if (!$this->ldap || !$this->ldap->ready) {
return null;
}
// Default filter of LDAP queries
$this->ldap_filter = $this->rc->config->get('kolab_delegation_filter', '(|(objectClass=kolabInetOrgPerson)(&(objectclass=kolabsharedfolder)(kolabFolderType=mail)))');
// Name of the LDAP field for delegates list
$this->ldap_delegate_field = $this->rc->config->get('kolab_delegation_delegate_field', 'kolabDelegate');
// Encoded LDAP DN of current user, set on login by kolab_auth plugin
$this->ldap_dn = $_SESSION['kolab_dn'];
// Name of the LDAP field with authentication ID
$this->ldap_login_field = $this->rc->config->get('kolab_delegation_login_field', $this->rc->config->get('kolab_auth_login'));
// Name of the LDAP field with user name used for identities
$this->ldap_name_field = $this->rc->config->get('kolab_delegation_name_field', $this->rc->config->get('kolab_auth_name'));
// Name of the LDAP field with email addresses used for identities
$this->ldap_email_field = $this->rc->config->get('kolab_delegation_email_field', $this->rc->config->get('kolab_auth_email'));
// Name of the LDAP field with organization name for identities
$this->ldap_org_field = $this->rc->config->get('kolab_delegation_organization_field', $this->rc->config->get('kolab_auth_organization'));
$this->ldap->set_filter($this->ldap_filter);
$this->ldap->extend_fieldmap(array($this->ldap_delegate_field => $this->ldap_delegate_field));
return $this->ldap;
}
/**
* List current user delegates
*/
public function list_delegates()
{
$result = array();
$ldap = $this->ldap();
$user = $this->user();
if (empty($ldap) || empty($user)) {
return array();
}
// Get delegates of current user
$delegates = $user[$this->ldap_delegate_field] ?? null;
if (!empty($delegates)) {
foreach ((array)$delegates as $dn) {
$delegate = $ldap->get_record($dn);
$data = $this->parse_ldap_record($delegate, $dn);
if (!empty($data) && !empty($data['name'])) {
$result[$data['ID']] = $data['name'];
}
}
}
return $result;
}
/**
* List current user delegators
*
* @return array List of delegators
*/
public function list_delegators()
{
$result = array();
$ldap = $this->ldap();
if (empty($ldap) || empty($this->ldap_dn)) {
return array();
}
$list = $ldap->dosearch($this->ldap_delegate_field, $this->ldap_dn, 1);
foreach ($list as $dn => $delegator) {
$delegator = $this->parse_ldap_record($delegator, $dn);
$result[$delegator['ID']] = $delegator;
}
return $result;
}
/**
* List current user delegators in format compatible with Calendar plugin
*
* @return array List of delegators
*/
public function list_delegators_js()
{
$list = $this->list_delegators();
$result = array();
foreach ($list as $delegator) {
$name = $delegator['name'];
if ($pos = strrpos($name, '(')) {
$name = trim(substr($name, 0, $pos));
}
$result[$delegator['imap_uid']] = array(
'emails' => ';' . implode(';', $delegator['email']),
'email' => $delegator['email'][0],
'name' => $name,
);
}
return $result;
}
/**
* Prepare namespace prefixes for JS environment
*
* @return array List of prefixes
*/
public function namespace_js()
{
$storage = $this->rc->get_storage();
$ns = $storage->get_namespace('other');
if ($ns) {
foreach ($ns as $idx => $nsval) {
$ns[$idx] = kolab_storage::folder_id($nsval[0]);
}
}
return $ns;
}
/**
* Get all folders to which current user has admin access
*
* @param string $delegate IMAP user identifier
*
* @return array Folder type/rights
*/
public function list_folders($delegate = null)
{
$storage = $this->rc->get_storage();
$folders = $storage->list_folders();
$metadata = kolab_storage::folders_typedata();
$result = array();
if (!is_array($metadata)) {
return $result;
}
// Definition of read and write ACL
$right_types = $this->right_types();
$delegate_lc = strtolower((string) $delegate);
foreach ($folders as $folder) {
// get only folders in personal namespace
if ($storage->folder_namespace($folder) != 'personal') {
continue;
}
$rights = null;
$type = !empty($metadata[$folder]) ? $metadata[$folder] : 'mail';
list($class, $subclass) = strpos($type, '.') ? explode('.', $type) : [$type, ''];
if (!in_array($class, $this->folder_types)) {
continue;
}
// in edit mode, get folder ACL
if ($delegate) {
// @TODO: cache ACL
$imap_acl = $storage->get_acl($folder);
if (!empty($imap_acl) && (($acl = ($imap_acl[$delegate] ?? null)) || ($acl = ($imap_acl[$delegate_lc] ?? null)))) {
if ($this->acl_compare($acl, $right_types[self::ACL_WRITE])) {
$rights = self::ACL_WRITE;
}
else if ($this->acl_compare($acl, $right_types[self::ACL_READ])) {
$rights = self::ACL_READ;
}
}
}
else if ($folder == 'INBOX' || $subclass == 'default' || $subclass == 'inbox') {
$rights = self::ACL_WRITE;
}
$result[$folder] = array(
'type' => $class,
'rights' => $rights,
);
}
return $result;
}
/**
* Returns list of users for autocompletion
*
* @param string $search Search string
*
* @return array Users list
*/
public function list_users($search)
{
$ldap = $this->ldap();
if (empty($ldap) || $search === '' || $search === null) {
return array();
}
$max = (int) $this->rc->config->get('autocomplete_max', 15);
$mode = (int) $this->rc->config->get('addressbook_search_mode');
$fields = array_unique(array_filter(array_merge((array)$this->ldap_name_field, (array)$this->ldap_login_field)));
$users = array();
$keys = array();
$result = $ldap->dosearch($fields, $search, $mode, (array)$this->ldap_login_field, $max);
foreach ($result as $record) {
// skip self
if ($record['dn'] == $_SESSION['kolab_dn']) {
continue;
}
$user = $this->parse_ldap_record($record);
if ($user['uid']) {
$display = rcube_addressbook::compose_search_name($record);
$user = array('name' => $user['uid'], 'display' => $display);
$users[] = $user;
$keys[] = $display ?: $user['uid'];
}
}
if (count($users)) {
// sort users index
asort($keys, SORT_LOCALE_STRING);
// re-sort users according to index
foreach (array_keys($keys) as $idx) {
$keys[$idx] = $users[$idx];
}
$users = array_values($keys);
}
return $users;
}
/**
* Extract delegate identifiers and pretty name from LDAP record
*/
private function parse_ldap_record($data, $dn = null)
{
$email = array();
$uid = $data[$this->ldap_login_field];
if (is_array($uid)) {
$uid = array_filter($uid);
$uid = $uid[0];
}
// User name for identity
foreach ((array)$this->ldap_name_field as $field) {
$name = is_array($data[$field]) ? $data[$field][0] : $data[$field];
if (!empty($name)) {
break;
}
}
// User email(s) for identity
foreach ((array)$this->ldap_email_field as $field) {
$user_email = is_array($data[$field]) ? array_filter($data[$field]) : $data[$field];
if (!empty($user_email)) {
$email = array_merge((array)$email, (array)$user_email);
}
}
// Organization for identity
foreach ((array)$this->ldap_org_field as $field) {
$organization = is_array($data[$field]) ? $data[$field][0] : $data[$field];
if (!empty($organization)) {
break;
}
}
$realname = $name;
if ($uid && $name) {
$name .= ' (' . $uid . ')';
}
else {
$name = $uid;
}
// get IMAP uid - identifier used in shared folder hierarchy
$imap_uid = $uid;
if ($pos = strpos($imap_uid, '@')) {
$imap_uid = substr($imap_uid, 0, $pos);
}
return array(
'ID' => kolab_ldap::dn_encode($dn),
'uid' => $uid,
'name' => $name,
'realname' => $realname,
'imap_uid' => $imap_uid,
'email' => $email,
'organization' => $organization ?? null,
);
}
/**
* Returns LDAP record of current user
*
* @return array User data
*/
public function user($parsed = false)
{
if (!isset($this->cache['user'])) {
$ldap = $this->ldap();
if (!$ldap) {
return array();
}
// Get current user record
$this->cache['user'] = $ldap->get_record($this->ldap_dn);
}
return $parsed ? $this->parse_ldap_record($this->cache['user']) : $this->cache['user'];
}
/**
* Update LDAP record of current user
*
* @param array List of delegates
*/
public function user_update_delegates($list)
{
$ldap = $this->ldap();
$pass = $this->rc->decrypt($_SESSION['password']);
if (!$ldap) {
return false;
}
// need to bind as self for sufficient privilages
if (!$ldap->bind($this->ldap_dn, $pass)) {
return false;
}
$user[$this->ldap_delegate_field] = $list;
unset($this->cache['user']);
// replace delegators list in user record
return $ldap->replace($this->ldap_dn, $user);
}
/**
* Manage delegation data on user login
*/
public function delegation_init()
{
// Fetch all delegators from LDAP who assigned the
// current user as their delegate and create identities
// a) if identity with delegator's email exists, continue
// b) create identity ($delegate on behalf of $delegator
// <$delegator-email>) for new delegators
// c) remove all other identities which do not match the user's primary
// or alias email if 'kolab_delegation_purge_identities' is set.
$delegators = $this->list_delegators();
$use_subs = $this->rc->config->get('kolab_use_subscriptions');
$identities = $this->rc->user->list_emails();
$emails = array();
$uids = array();
if (!empty($delegators)) {
$storage = $this->rc->get_storage();
$other_ns = $storage->get_namespace('other') ?: array();
$folders = $storage->list_folders();
}
// convert identities to simpler format for faster access
foreach ($identities as $idx => $ident) {
// get user name from default identity
if (!$idx) {
$default = array(
'name' => $ident['name'],
);
}
$emails[$ident['identity_id']] = $ident['email'];
}
// for every delegator...
foreach ($delegators as $delegator) {
$uids[$delegator['imap_uid']] = $email_arr = $delegator['email'];
$diff = array_intersect($emails, $email_arr);
// identity with delegator's email already exist, do nothing
if (count($diff)) {
$emails = array_diff($emails, $email_arr);
continue;
}
// create identities for delegator emails
foreach ($email_arr as $email) {
// @TODO: "Delegatorname" or "Username on behalf of Delegatorname"?
$default['name'] = $delegator['realname'];
$default['email'] = $email;
// Database field for organization is NOT NULL
$default['organization'] = empty($delegator['organization']) ? '' : $delegator['organization'];
$this->rc->user->insert_identity($default);
}
// IMAP folders shared by new delegators shall be subscribed on login,
// as well as existing subscriptions of previously shared folders shall
// be removed. I suppose the latter one is already done in Roundcube.
// for every accessible folder...
foreach ($folders as $folder) {
// for every 'other' namespace root...
foreach ($other_ns as $ns) {
$prefix = $ns[0] . $delegator['imap_uid'];
// subscribe delegator's folder
if ($folder === $prefix || strpos($folder, $prefix . substr($ns[0], -1)) === 0) {
// Event/Task folders need client-side activation
$type = kolab_storage::folder_type($folder);
if (preg_match('/^(event|task)/i', $type)) {
kolab_storage::folder_activate($folder);
}
// Subscribe to mail folders and (if system is configured
// to display only subscribed folders) to other
if ($use_subs || preg_match('/^mail/i', $type)) {
$storage->subscribe($folder);
}
}
}
}
}
// remove identities that "do not belong" to user nor delegators
if ($this->rc->config->get('kolab_delegation_purge_identities')) {
$user = $this->user(true);
$emails = array_diff($emails, $user['email']);
foreach (array_keys($emails) as $idx) {
$this->rc->user->delete_identity($idx);
}
}
$_SESSION['delegators'] = $uids;
}
/**
* Sets delegator context according to email message recipient
*
* @param rcube_message $message Email message object
*/
public function delegator_context_from_message($message)
{
if (empty($_SESSION['delegators'])) {
return;
}
// Match delegators' addresses with message To: address
// @TODO: Is this reliable enough?
// Roundcube sends invitations to every attendee separately,
// but maybe there's a software which sends with CC header or many addresses in To:
$emails = $message->get_header('to');
$emails = rcube_mime::decode_address_list($emails, null, false);
foreach ($emails as $email) {
foreach ($_SESSION['delegators'] as $uid => $addresses) {
if (in_array($email['mailto'], $addresses)) {
return $this->context = $uid;
}
}
}
}
/**
* Return (set) current delegator context
*
* @return string Delegator UID
*/
public function delegator_context()
{
if (!$this->context && !empty($_SESSION['delegators'])) {
$context = rcube_utils::get_input_value('_context', rcube_utils::INPUT_GPC);
if ($context && isset($_SESSION['delegators'][$context])) {
$this->context = $context;
}
}
return $this->context;
}
/**
* Set user identity according to delegator delegator
*
* @param array $args Reference to plugin hook arguments
*/
public function delegator_identity_filter(&$args)
{
$context = $this->delegator_context();
if (!$context) {
return;
}
$identities = $this->rc->user->list_emails();
$emails = $_SESSION['delegators'][$context];
foreach ($identities as $ident) {
if (in_array($ident['email'], $emails)) {
$args['identity'] = $ident;
return;
}
}
// fallback to default identity
$args['identity'] = array_shift($identities);
}
/**
* Filter user emails according to delegator context
*
* @param array $args Reference to plugin hook arguments
*/
public function delegator_emails_filter(&$args)
{
$context = $this->delegator_context();
// try to derive context from the given user email
if (!$context && !empty($args['emails'])) {
if (($user = preg_replace('/@.+$/', '', $args['emails'][0])) && isset($_SESSION['delegators'][$user])) {
$context = $user;
}
}
// return delegator's addresses
if ($context) {
$args['emails'] = $_SESSION['delegators'][$context];
$args['abort'] = true;
}
// return only user addresses (exclude all delegators addresses)
else if (!empty($_SESSION['delegators'])) {
$identities = $this->rc->user->list_emails();
$emails[] = $this->rc->user->get_username();
foreach ($identities as $identity) {
$emails[] = $identity['email'];
}
foreach ($_SESSION['delegators'] as $delegator_emails) {
$emails = array_diff($emails, $delegator_emails);
}
$args['emails'] = array_unique($emails);
$args['abort'] = true;
}
}
/**
* Filters list of calendar/task folders according to delegator context
*
* @param array $args Reference to plugin hook arguments
*/
public function delegator_folder_filter(&$args, $mode = 'calendars')
{
$context = $this->delegator_context();
if (empty($context)) {
return $args;
}
$storage = $this->rc->get_storage();
$other_ns = $storage->get_namespace('other') ?: array();
$delim = $storage->get_hierarchy_delimiter();
if ($mode == 'calendars') {
$editable = $args['filter'] & calendar_driver::FILTER_WRITEABLE;
$active = $args['filter'] & calendar_driver::FILTER_ACTIVE;
$personal = $args['filter'] & calendar_driver::FILTER_PERSONAL;
$shared = $args['filter'] & calendar_driver::FILTER_SHARED;
}
else {
$editable = $args['filter'] & tasklist_driver::FILTER_WRITEABLE;
$active = $args['filter'] & tasklist_driver::FILTER_ACTIVE;
$personal = $args['filter'] & tasklist_driver::FILTER_PERSONAL;
$shared = $args['filter'] & tasklist_driver::FILTER_SHARED;
}
$folders = array();
foreach ($args['list'] as $folder) {
if (isset($folder->ready) && !$folder->ready) {
continue;
}
if ($editable && !$folder->editable) {
continue;
}
if ($active && !$folder->storage->is_active()) {
continue;
}
if ($personal || $shared) {
$ns = $folder->get_namespace();
if ($personal && $ns == 'personal') {
continue;
}
else if ($personal && $ns == 'other') {
$found = false;
foreach ($other_ns as $ns) {
$c_folder = $ns[0] . $context . $delim;
if (strpos($folder->name, $c_folder) === 0) {
$found = true;
}
}
if (!$found) {
continue;
}
}
else if (!$shared || $ns != 'shared') {
continue;
}
}
$folders[$folder->id] = $folder;
}
$args[$mode] = $folders;
$args['abort'] = true;
}
/**
* Filters/updates message headers according to delegator context
*
* @param array $args Reference to plugin hook arguments
*/
public function delegator_delivery_filter(&$args)
{
// no context, but message still can be send on behalf of...
if (!empty($_SESSION['delegators'])) {
$message = $args['message'];
$headers = $message->headers();
// get email address from From: header
$from = rcube_mime::decode_address_list($headers['From']);
$from = array_shift($from);
$from = $from['mailto'];
foreach ($_SESSION['delegators'] as $uid => $addresses) {
if (in_array($from, $addresses)) {
$context = $uid;
break;
}
}
// add Sender: header with current user default identity
if (!empty($context)) {
$identity = $this->rc->user->get_identity();
$sender = format_email_recipient($identity['email'], $identity['name']);
$message->headers(array('Sender' => $sender), false, true);
}
}
}
/**
* Compares two ACLs (according to supported rights)
*
* @param array $acl1 ACL rights array (or string)
* @param array $acl2 ACL rights array (or string)
*
* @param bool True if $acl1 contains all rights from $acl2
*/
function acl_compare($acl1, $acl2)
{
if (!is_array($acl1)) $acl1 = str_split($acl1);
if (!is_array($acl2)) $acl2 = str_split($acl2);
$rights = $this->rights_supported();
$acl1 = array_intersect($acl1, $rights);
$acl2 = array_intersect($acl2, $rights);
$res = array_intersect($acl1, $acl2);
$cnt1 = count($res);
$cnt2 = count($acl2);
if ($cnt1 >= $cnt2) {
return true;
}
}
/**
* Get list of supported access rights (according to RIGHTS capability)
*
* @todo: this is stolen from acl plugin, move to rcube_storage/rcube_imap
*
* @return array List of supported access rights abbreviations
*/
public function rights_supported()
{
if ($this->supported !== null) {
return $this->supported;
}
$storage = $this->rc->get_storage();
$capa = $storage->get_capability('RIGHTS');
if (is_array($capa)) {
$rights = strtolower($capa[0]);
}
else {
$rights = 'cd';
}
return $this->supported = str_split('lrswi' . $rights . 'pa');
}
private function right_types()
{
// Get supported rights and build column names
$supported = $this->rights_supported();
// depending on server capability either use 'te' or 'd' for deleting msgs
$deleteright = implode(array_intersect(str_split('ted'), $supported));
return array(
self::ACL_READ => 'lrs',
self::ACL_WRITE => 'lrswi'.$deleteright,
);
}
}
diff --git a/plugins/kolab_folders/kolab_folders.php b/plugins/kolab_folders/kolab_folders.php
index a6f7fb0f..340901fe 100644
--- a/plugins/kolab_folders/kolab_folders.php
+++ b/plugins/kolab_folders/kolab_folders.php
@@ -1,858 +1,858 @@
<?php
/**
* Type-aware folder management/listing for Kolab
*
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2017, 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_folders extends rcube_plugin
{
public $task = '?(?!login).*';
public $types = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration', 'file', 'freebusy');
public $subtypes = array(
'mail' => array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail'),
'event' => array('default'),
'task' => array('default'),
'journal' => array('default'),
'note' => array('default'),
'contact' => array('default'),
'configuration' => array('default'),
'file' => array('default'),
'freebusy' => array('default'),
);
public $act_types = array('event', 'task');
private $rc;
private static $instance;
private $expire_annotation = '/shared/vendor/cmu/cyrus-imapd/expire';
private $is_processing = false;
/**
* Plugin initialization.
*/
function init()
{
self::$instance = $this;
$this->rc = rcube::get_instance();
// load required plugin
$this->require_plugin('libkolab');
// Folder listing hooks
$this->add_hook('storage_folders', array($this, 'mailboxes_list'));
// Folder manager hooks
$this->add_hook('folder_form', array($this, 'folder_form'));
$this->add_hook('folder_update', array($this, 'folder_save'));
$this->add_hook('folder_create', array($this, 'folder_save'));
$this->add_hook('folder_delete', array($this, 'folder_save'));
$this->add_hook('folder_rename', array($this, 'folder_save'));
$this->add_hook('folders_list', array($this, 'folders_list'));
// Special folders setting
$this->add_hook('preferences_save', array($this, 'prefs_save'));
// ACL plugin hooks
$this->add_hook('acl_rights_simple', array($this, 'acl_rights_simple'));
$this->add_hook('acl_rights_supported', array($this, 'acl_rights_supported'));
// Resolving other user folder names
$this->add_hook('render_mailboxlist', array($this, 'render_folderlist'));
$this->add_hook('render_folder_selector', array($this, 'render_folderlist'));
$this->add_hook('folders_list', array($this, 'render_folderlist'));
}
/**
* Handler for mailboxes_list hook. Enables type-aware lists filtering.
*/
function mailboxes_list($args)
{
// infinite loop prevention
if ($this->is_processing) {
return $args;
}
if (!$this->metadata_support()) {
return $args;
}
$this->is_processing = true;
// get folders
$folders = kolab_storage::list_folders($args['root'], $args['name'], $args['filter'], $args['mode'] == 'LSUB', $folderdata);
$this->is_processing = false;
if (!is_array($folders)) {
return $args;
}
// Create default folders
if ($args['root'] == '' && $args['name'] = '*') {
$this->create_default_folders($folders, $args['filter'], $folderdata, $args['mode'] == 'LSUB');
}
$args['folders'] = $folders;
return $args;
}
/**
* Handler for folders_list hook. Add css classes to folder rows.
*/
function folders_list($args)
{
if (!$this->metadata_support()) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// Add javascript script to the client
$this->include_script('kolab_folders.js');
$this->add_label('folderctype');
foreach ($this->types as $type) {
$this->add_label('foldertype' . $type);
}
$skip_namespace = $this->rc->config->get('kolab_skip_namespace');
$skip_roots = array();
if (!empty($skip_namespace)) {
$storage = $this->rc->get_storage();
foreach ((array)$skip_namespace as $ns) {
foreach((array)$storage->get_namespace($ns) as $root) {
$skip_roots[] = rtrim($root[0], $root[1]);
}
}
}
$this->rc->output->set_env('skip_roots', $skip_roots);
$this->rc->output->set_env('foldertypes', $this->types);
// get folders types
$folderdata = kolab_storage::folders_typedata();
if (!is_array($folderdata)) {
return $args;
}
// Add type-based style for table rows
// See kolab_folders::folder_class_name()
if (!empty($args['table'])) {
$table = $args['table'];
for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
$attrib = $table->get_row_attribs($i);
$folder = $attrib['foldername']; // UTF7-IMAP
$type = $folderdata[$folder];
if (!$type) {
$type = 'mail';
}
$class_name = self::folder_class_name($type);
$attrib['class'] = trim($attrib['class'] . ' ' . $class_name);
$table->set_row_attribs($attrib, $i);
}
}
// Add type-based class for list items
if (!empty($args['list']) && is_array($args['list'])) {
foreach ($args['list'] as $k => $item) {
$folder = $item['folder_imap']; // UTF7-IMAP
$type = $folderdata[$folder] ?? null;
if (!$type) {
$type = 'mail';
}
$class_name = self::folder_class_name($type);
$args['list'][$k]['class'] = trim($item['class'] . ' ' . $class_name);
}
}
return $args;
}
/**
* Handler for folder info/edit form (folder_form hook).
* Adds folder type selector.
*/
function folder_form($args)
{
if (!$this->metadata_support()) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// INBOX folder is of type mail.inbox and this cannot be changed
if ($args['name'] == 'INBOX') {
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => sprintf('%s (%s)', $this->gettext('foldertypemail'), $this->gettext('inbox')),
);
$this->add_expire_input($args['form'], 'INBOX');
return $args;
}
if (!empty($args['options']['is_root'])) {
return $args;
}
$mbox = strlen($args['name']) ? $args['name'] : $args['parent_name'];
if (isset($_POST['_ctype'])) {
$new_ctype = trim(rcube_utils::get_input_value('_ctype', rcube_utils::INPUT_POST));
$new_subtype = trim(rcube_utils::get_input_value('_subtype', rcube_utils::INPUT_POST));
}
// Get type of the folder or the parent
$subtype = '';
if (strlen($mbox)) {
list($ctype, $subtype) = $this->get_folder_type($mbox);
- if (strlen($args['parent_name']) && $subtype == 'default') {
+ if (isset($args['parent_name']) && strlen($args['parent_name']) && $subtype == 'default') {
$subtype = ''; // there can be only one
}
}
if (empty($ctype)) {
$ctype = 'mail';
}
$storage = $this->rc->get_storage();
// Don't allow changing type of shared folder, according to ACL
if (strlen($mbox)) {
$options = $storage->folder_info($mbox);
if ($options['namespace'] != 'personal' && !in_array('a', (array)$options['rights'])) {
if (in_array($ctype, $this->types)) {
$value = $this->gettext('foldertype'.$ctype);
}
else {
$value = $ctype;
}
if ($subtype) {
$value .= ' ('. ($subtype == 'default' ? $this->gettext('default') : $subtype) .')';
}
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => $value,
);
return $args;
}
}
// Add javascript script to the client
$this->include_script('kolab_folders.js');
// build type SELECT fields
$type_select = new html_select(array('name' => '_ctype', 'id' => '_folderctype',
'onchange' => "\$('[name=\"_expire\"]').attr('disabled', \$(this).val() != 'mail')"
));
$sub_select = new html_select(array('name' => '_subtype', 'id' => '_subtype'));
$sub_select->add('', '');
foreach ($this->types as $type) {
$type_select->add($this->gettext('foldertype'.$type), $type);
}
// add non-supported type
if (!in_array($ctype, $this->types)) {
$type_select->add($ctype, $ctype);
}
$sub_types = array();
foreach ($this->subtypes as $ftype => $subtypes) {
$sub_types[$ftype] = array_combine($subtypes, array_map(array($this, 'gettext'), $subtypes));
// fill options for the current folder type
if ($ftype == $ctype || (isset($new_ctype) && $ftype == $new_ctype)) {
$sub_select->add(array_values($sub_types[$ftype]), $subtypes);
}
}
$args['form']['props']['fieldsets']['settings']['content']['folderctype'] = array(
'label' => $this->gettext('folderctype'),
'value' => html::div('input-group',
$type_select->show($new_ctype ?? $ctype)
. $sub_select->show($new_subtype ?? $subtype)
),
);
$this->rc->output->set_env('kolab_folder_subtypes', $sub_types);
$this->rc->output->set_env('kolab_folder_subtype', $new_subtype ?? $subtype);
$this->add_expire_input($args['form'], $args['name'], $ctype);
return $args;
}
/**
* Handler for folder update/create action (folder_update/folder_create hook).
*/
function folder_save($args)
{
// Folder actions from folders list
if (empty($args['record'])) {
return $args;
}
// Folder create/update with form
$ctype = trim(rcube_utils::get_input_value('_ctype', rcube_utils::INPUT_POST));
$subtype = trim(rcube_utils::get_input_value('_subtype', rcube_utils::INPUT_POST));
$mbox = $args['record']['name'];
$old_mbox = $args['record']['oldname'] ?? null;
$subscribe = $args['record']['subscribe'] ?? true;
if (empty($ctype)) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// Skip folder creation/rename in core
// @TODO: Maybe we should provide folder_create_after and folder_update_after hooks?
// Using create_mailbox/rename_mailbox here looks bad
$args['abort'] = true;
// There can be only one default folder of specified type
if ($subtype == 'default') {
$default = $this->get_default_folder($ctype);
if ($default !== null && $old_mbox != $default) {
$args['result'] = false;
$args['message'] = $this->gettext('defaultfolderexists');
return $args;
}
}
// Subtype sanity-checks
else if ($subtype && (!($subtypes = $this->subtypes[$ctype]) || !in_array($subtype, $subtypes))) {
$subtype = '';
}
$ctype .= $subtype ? '.'.$subtype : '';
$storage = $this->rc->get_storage();
// Create folder
if (!strlen($old_mbox)) {
$result = $storage->create_folder($mbox, $subscribe);
// Set folder type
if ($result) {
$this->set_folder_type($mbox, $ctype);
}
}
// Rename folder
else {
if ($old_mbox != $mbox) {
$result = $storage->rename_folder($old_mbox, $mbox);
}
else {
$result = true;
}
if ($result) {
list($oldtype, $oldsubtype) = $this->get_folder_type($mbox);
$oldtype .= $oldsubtype ? '.'.$oldsubtype : '';
if ($ctype != $oldtype) {
$this->set_folder_type($mbox, $ctype);
}
}
}
// Set messages expiration in days
if ($result && isset($_POST['_expire'])) {
$expire = trim(rcube_utils::get_input_value('_expire', rcube_utils::INPUT_POST));
$expire = intval($expire) && preg_match('/^mail/', $ctype) ? intval($expire) : null;
$storage->set_metadata($mbox, array($this->expire_annotation => $expire));
}
$args['record']['class'] = self::folder_class_name($ctype);
$args['record']['subscribe'] = $subscribe;
$args['result'] = $result;
return $args;
}
/**
* Handler for user preferences save (preferences_save hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_save($args)
{
if ($args['section'] != 'folders') {
return $args;
}
$dont_override = (array) $this->rc->config->get('dont_override', array());
// map config option name to kolab folder type annotation
$opts = array(
'drafts_mbox' => 'mail.drafts',
'sent_mbox' => 'mail.sentitems',
'junk_mbox' => 'mail.junkemail',
'trash_mbox' => 'mail.wastebasket',
);
// check if any of special folders has been changed
foreach ($opts as $opt_name => $type) {
$new = $args['prefs'][$opt_name];
$old = $this->rc->config->get($opt_name);
if (!strlen($new) || $new === $old || in_array($opt_name, $dont_override)) {
unset($opts[$opt_name]);
}
}
if (empty($opts)) {
return $args;
}
$folderdata = kolab_storage::folders_typedata();
if (!is_array($folderdata)) {
return $args;
}
foreach ($opts as $opt_name => $type) {
$foldername = $args['prefs'][$opt_name];
// get all folders of specified type
$folders = array_intersect($folderdata, array($type));
// folder already annotated with specified type
if (!empty($folders[$foldername])) {
continue;
}
// set type to the new folder
$this->set_folder_type($foldername, $type);
// unset old folder(s) type annotation
list($maintype, $subtype) = explode('.', $type);
foreach (array_keys($folders) as $folder) {
$this->set_folder_type($folder, $maintype);
}
}
return $args;
}
/**
* Handler for ACL permissions listing (acl_rights_simple hook)
*
* This shall combine the write and delete permissions into one item for
* groupware folders as updating groupware objects is an insert + delete operation.
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function acl_rights_simple($args)
{
if ($args['folder']) {
list($type,) = $this->get_folder_type($args['folder']);
// we're dealing with a groupware folder here...
if ($type && $type !== 'mail') {
if ($args['rights']['write'] && $args['rights']['delete']) {
$write_perms = $args['rights']['write'] . $args['rights']['delete'];
$rw_perms = $write_perms . $args['rights']['read'];
$args['rights']['write'] = $write_perms;
$args['rights']['other'] = preg_replace("/[$rw_perms]/", '', $args['rights']['other']);
// add localized labels and titles for the altered items
$args['labels'] = array(
'other' => $this->rc->gettext('shortacla','acl'),
);
$args['titles'] = array(
'other' => $this->rc->gettext('longaclother','acl'),
);
}
}
}
return $args;
}
/**
* Handler for ACL permissions listing (acl_rights_supported hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function acl_rights_supported($args)
{
if ($args['folder']) {
list($type,) = $this->get_folder_type($args['folder']);
// we're dealing with a groupware folder here...
if ($type && $type !== 'mail') {
// remove some irrelevant (for groupware objects) rights
$args['rights'] = str_split(preg_replace('/[p]/', '', join('', $args['rights'])));
}
}
return $args;
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @return bool
*/
function metadata_support()
{
$storage = $this->rc->get_storage();
return $storage->get_capability('METADATA') ||
$storage->get_capability('ANNOTATEMORE') ||
$storage->get_capability('ANNOTATEMORE2');
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @param string $folder Folder name
*
* @return array Folder content-type
*/
function get_folder_type($folder)
{
$type = explode('.', (string)kolab_storage::folder_type($folder));
if (!isset($type[1])) {
$type[1] = null;
}
return $type;
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return bool True on success
*/
function set_folder_type($folder, $type = 'mail')
{
return kolab_storage::set_folder_type($folder, $type);
}
/**
* Returns the name of default folder
*
* @param string $type Folder type
*
* @return string Folder name
*/
function get_default_folder($type)
{
$folderdata = kolab_storage::folders_typedata();
if (!is_array($folderdata)) {
return null;
}
// get all folders of specified type
$folderdata = array_intersect($folderdata, array($type.'.default'));
return key($folderdata);
}
/**
* Returns CSS class name for specified folder type
*
* @param string $type Folder type
*
* @return string Class name
*/
static function folder_class_name($type)
{
if ($type && strpos($type, '.')) {
list($ctype, $subtype) = explode('.', $type);
return 'type-' . $ctype . ' subtype-' . $subtype;
}
return 'type-' . ($type ? $type : 'mail');
}
/**
* Creates default folders if they doesn't exist
*/
private function create_default_folders(&$folders, $filter, $folderdata = null, $lsub = false)
{
$storage = $this->rc->get_storage();
$namespace = $storage->get_namespace();
$defaults = array();
$prefix = '';
// Find personal namespace prefix
if (is_array($namespace['personal']) && count($namespace['personal']) == 1) {
$prefix = $namespace['personal'][0][0];
}
$this->load_config();
// get configured defaults
foreach ($this->types as $type) {
foreach ((array)$this->subtypes[$type] as $subtype) {
$opt_name = 'kolab_folders_' . $type . '_' . $subtype;
if ($folder = $this->rc->config->get($opt_name)) {
// convert configuration value to UTF7-IMAP charset
$folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP');
// and namespace prefix if needed
if ($prefix && strpos($folder, $prefix) === false && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
$defaults[$type . '.' . $subtype] = $folder;
}
}
}
if (empty($defaults)) {
return;
}
if ($folderdata === null) {
$folderdata = kolab_storage::folders_typedata();
}
if (!is_array($folderdata)) {
return;
}
// find default folders
foreach ($defaults as $type => $foldername) {
// get all folders of specified type
$_folders = array_intersect($folderdata, array($type));
// default folder found
if (!empty($_folders)) {
continue;
}
list($type1, $type2) = explode('.', $type);
$activate = in_array($type1, $this->act_types);
$exists = false;
$result = false;
// check if folder exists
if (!empty($folderdata[$foldername]) || $foldername == 'INBOX') {
$exists = true;
}
else if ((!$filter || $filter == $type1) && in_array($foldername, $folders)) {
// this assumes also that subscribed folder exists
$exists = true;
}
else {
$exists = $storage->folder_exists($foldername);
}
// create folder
if (!$exists) {
$exists = $storage->create_folder($foldername);
}
// set type + subscribe + activate
if ($exists) {
if ($result = kolab_storage::set_folder_type($foldername, $type)) {
// check if folder is subscribed
if ((!$filter || $filter == $type1) && $lsub && in_array($foldername, $folders)) {
// already subscribed
$subscribed = true;
}
else {
$subscribed = $storage->subscribe($foldername);
}
// activate folder
if ($activate) {
kolab_storage::folder_activate($foldername, true);
}
}
}
// add new folder to the result
if ($result && (!$filter || $filter == $type1) && (!$lsub || $subscribed)) {
$folders[] = $foldername;
}
}
}
/**
* Static getter for default folder of the given type
*
* @param string $type Folder type
*
* @return string Folder name
*/
public static function default_folder($type)
{
return self::$instance->get_default_folder($type);
}
/**
* Get /shared/vendor/cmu/cyrus-imapd/expire value
*
* @param string $folder IMAP folder name
*
* @return int|false The annotation value or False if not supported
*/
private function get_expire_annotation($folder)
{
$storage = $this->rc->get_storage();
if ($storage->get_vendor() != 'cyrus') {
return false;
}
if (!strlen($folder)) {
return 0;
}
$value = $storage->get_metadata($folder, $this->expire_annotation);
if (is_array($value)) {
return !empty($value[$folder]) ? intval($value[$folder][$this->expire_annotation] ?? 0) : 0;
}
return false;
}
/**
* Add expiration time input to the form if supported
*/
private function add_expire_input(&$form, $folder, $type = null)
{
if (($expire = $this->get_expire_annotation($folder)) !== false) {
$post = trim(rcube_utils::get_input_value('_expire', rcube_utils::INPUT_POST));
$is_mail = empty($type) || preg_match('/^mail/i', $type);
$label = $this->gettext('xdays');
$input = new html_inputfield(array(
'id' => '_kolabexpire',
'name' => '_expire',
'size' => 3,
'disabled' => !$is_mail
));
if ($post && $is_mail) {
$expire = (int) $post;
}
if (strpos($label, '$') === 0) {
$label = str_replace('$x', '', $label);
$html = $input->show($expire ?: '')
. html::span('input-group-append', html::span('input-group-text', rcube::Q($label)));
}
else {
$label = str_replace('$x', '', $label);
$html = html::span('input-group-prepend', html::span('input-group-text', rcube::Q($label)))
. $input->show($expire ?: '');
}
$form['props']['fieldsets']['settings']['content']['kolabexpire'] = array(
'label' => $this->gettext('folderexpire'),
'value' => html::div('input-group', $html),
);
}
}
/**
* Handler for various folders list widgets (hooks)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function render_folderlist($args)
{
$storage = $this->rc->get_storage();
$ns_other = $storage->get_namespace('other');
$is_fl = $this->rc->plugins->is_processing('folders_list');
foreach ((array) $ns_other as $root) {
$delim = $root[1];
$prefix = rtrim($root[0], $delim);
$length = strlen($prefix);
if (!$length) {
continue;
}
// folders_list hook mode
if ($is_fl) {
foreach ((array) $args['list'] as $folder_name => $folder) {
if (strpos($folder_name, $root[0]) === 0 && !substr_count($folder_name, $root[1], $length+1)) {
if ($name = kolab_storage::folder_id2user(substr($folder_name, $length+1), true)) {
$old = $args['list'][$folder_name]['display'];
$content = $args['list'][$folder_name]['content'];
$name = rcube::Q($name);
$content = str_replace(">$old<", ">$name<", $content);
$args['list'][$folder_name]['display'] = $name;
$args['list'][$folder_name]['content'] = $content;
}
}
}
// TODO: Re-sort the list
}
// render_* hooks mode
else if (!empty($args['list'][$prefix]) && !empty($args['list'][$prefix]['folders'])) {
$map = array();
foreach ($args['list'][$prefix]['folders'] as $folder_name => $folder) {
if ($name = kolab_storage::folder_id2user($folder_name, true)) {
$args['list'][$prefix]['folders'][$folder_name]['name'] = $name;
}
$map[$folder_name] = $name ?: $args['list'][$prefix]['folders'][$folder_name]['name'];
}
// Re-sort the list
uasort($map, 'strcoll');
$args['list'][$prefix]['folders'] = array_replace($map, $args['list'][$prefix]['folders']);
}
}
return $args;
}
}
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 893af60b..9f4791e5 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1,1823 +1,1823 @@
<?php
/**
* Kolab storage class providing static methods to access groupware objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@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
* 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
{
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
const NAME_KEY_SHARED = '/shared/vendor/kolab/displayname';
const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname';
const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid';
const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
const ERROR_IMAP_CONN = 1;
const ERROR_CACHE_DB = 2;
const ERROR_NO_PERMISSION = 3;
const ERROR_INVALID_FOLDER = 4;
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 $ldapcache = array();
private static $ldap = array();
private static $states;
private static $config;
private static $imap;
// Default folder names
private static $default_folders = array(
'event' => 'Calendar',
'contact' => 'Contacts',
'task' => 'Tasks',
'note' => 'Notes',
'file' => 'Files',
'configuration' => 'Configuration',
'journal' => 'Journal',
'mail.inbox' => 'INBOX',
'mail.drafts' => 'Drafts',
'mail.sentitems' => 'Sent',
'mail.wastebasket' => 'Trash',
'mail.outbox' => 'Outbox',
'mail.junkemail' => 'Junk',
);
/**
* Setup the environment needed by the libs
*/
public static function setup()
{
if (self::$ready)
return true;
$rcmail = rcube::get_instance();
self::$config = $rcmail->config;
self::$version = strval($rcmail->config->get('kolab_format_version', self::$version));
self::$imap = $rcmail->get_storage();
self::$ready = class_exists('kolabformat') &&
(self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
if (self::$ready) {
// do nothing
}
else if (!class_exists('kolabformat')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabformat module not found"
), true);
}
else if (self::$imap->get_error_code()) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php', 'message' => "IMAP error"
), true);
}
// adjust some configurable settings
if ($event_scheduling_prop = $rcmail->config->get('kolab_event_scheduling_properties', null)) {
kolab_format_event::$scheduling_properties = (array)$event_scheduling_prop;
}
// adjust some configurable settings
if ($task_scheduling_prop = $rcmail->config->get('kolab_task_scheduling_properties', null)) {
kolab_format_task::$scheduling_properties = (array)$task_scheduling_prop;
}
return self::$ready;
}
/**
* Initializes LDAP object to resolve Kolab users
*
* @param string $name Name of the configuration option with LDAP config
*/
public static function ldap($name = 'kolab_users_directory')
{
self::setup();
$config = self::$config->get($name);
if (empty($config)) {
$name = 'kolab_auth_addressbook';
$config = self::$config->get($name);
}
if (!empty(self::$ldap[$name])) {
return self::$ldap[$name];
}
if (!is_array($config)) {
$ldap_config = (array)self::$config->get('ldap_public');
$config = $ldap_config[$config];
}
if (empty($config)) {
return null;
}
$ldap = new kolab_ldap($config);
// overwrite filter option
if ($filter = self::$config->get('kolab_users_filter')) {
self::$config->set('kolab_auth_filter', $filter);
}
$user_field = $user_attrib = self::$config->get('kolab_users_id_attrib');
// Fallback to kolab_auth_login, which is not attribute, but field name
if (!$user_field && ($user_field = self::$config->get('kolab_auth_login', 'email'))) {
$user_attrib = $config['fieldmap'][$user_field];
}
if ($user_field && $user_attrib) {
$ldap->extend_fieldmap(array($user_field => $user_attrib));
}
self::$ldap[$name] = $ldap;
return $ldap;
}
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @param bool Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type, $subscribed = null)
{
$folders = $folderdata = array();
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
$folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Getter for the storage folder for the given type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @return object kolab_storage_folder The folder object
*/
public static function get_default_folder($type)
{
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return null;
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @param string Expected folder type
*
* @return object kolab_storage_folder The folder object
*/
public static function get_folder($folder, $type = null)
{
return self::setup() ? new kolab_storage_folder($folder, $type) : null;
}
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,event,task,journal,file,note,configuration)
* @return array The Kolab object represented as hash array or false if not found
*/
public static function get_object($uid, $type)
{
self::setup();
$folder = null;
foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
if (!$folder)
$folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
else
$folder->set_folder($foldername, $type, $folderdata[$foldername]);
if ($object = $folder->get_object($uid))
return $object;
}
return false;
}
/**
* Execute cross-folder searches with the given query.
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @param string Folder type (contact,event,task,journal,file,note,configuration)
* @param int Expected number of records or limit (for performance reasons)
*
* @return array List of Kolab data objects (each represented as hash array)
* @see kolab_storage_format::select()
*/
public static function select($query, $type, $limit = null)
{
self::setup();
$folder = null;
$result = array();
foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
$folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
if ($limit) {
$folder->set_order_and_limit(null, $limit);
}
foreach ($folder->select($query) as $object) {
$result[] = $object;
}
}
return $result;
}
/**
* Returns Free-busy server URL
*/
public static function get_freebusy_server()
{
$rcmail = rcube::get_instance();
if ($url = $rcmail->config->get('kolab_freebusy_server')) {
$url = rcube_utils::resolve_url($url);
$url = unslashify($url);
}
return $url;
}
/**
* Compose an URL to query the free/busy status for the given user
*
* @param string Email address of the user to get free/busy data for
* @param object DateTime Start of the query range (optional)
* @param object DateTime End of the query range (optional)
*
* @return ?string Fully qualified URL to query free/busy data
*/
public static function get_freebusy_url($email, $start = null, $end = null)
{
$url = self::get_freebusy_server();
if (empty($url)) {
return null;
}
$query = '';
$param = array();
$utc = new \DateTimeZone('UTC');
// https://www.calconnect.org/pubdocs/CD0903%20Freebusy%20Read%20URL.pdf
if ($start instanceof \DateTime) {
$start->setTimezone($utc);
$param['start'] = $param['dtstart'] = $start->format('Ymd\THis\Z');
}
if ($end instanceof \DateTime) {
$end->setTimezone($utc);
$param['end'] = $param['dtend'] = $end->format('Ymd\THis\Z');
}
if (!empty($param)) {
$query = '?' . http_build_query($param);
}
if (strpos($url, '%u')) {
// Expected configured full URL, just replace the %u variable
// Note: Cyrus v3 Free-Busy service does not use .ifb extension
$url = str_replace('%u', rawurlencode($email), $url);
}
else {
$url .= '/' . $email . '.ifb';
}
return $url . $query;
}
/**
* Creates folder ID from folder name
*
* @param string $folder Folder name (UTF7-IMAP)
* @param bool $enc Use lossless encoding
*
* @return string Folder ID string
*/
public static function folder_id($folder, $enc = null)
{
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)
{
self::setup();
foreach ((array)self::$imap->get_namespace($name) as $paths) {
if (strlen($paths[0]) > 1) {
return $paths[0];
}
}
return '';
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
// clear cached entries first
if ($folder = self::get_folder($name))
$folder->cache->purge();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
$success = self::$imap->delete_folder($name);
self::$last_error = self::$imap->get_error_str();
return $success;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type = null, $subscribed = false, $active = false)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
'name' => $name,
'subscribe' => $subscribed,
)));
if ($saved = self::$imap->create_folder($name, $subscribed)) {
// set metadata for folder type
if ($type) {
$saved = self::set_folder_type($name, $type);
// revert if metadata could not be set
if (!$saved) {
self::$imap->delete_folder($name);
}
// activate folder
else if ($active) {
self::set_state($name, true);
}
}
}
if ($saved) {
return true;
}
self::$last_error = self::$imap->get_error_str();
return false;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_rename', array(
'oldname' => $oldname, 'newname' => $newname));
$oldfolder = self::get_folder($oldname);
$active = self::folder_is_active($oldname);
$success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
// pass active state to new folder name
if ($success && $active) {
self::set_state($oldname, false);
self::set_state($newname, true);
}
// assign existing cache entries to new resource uri
if ($success && $oldfolder) {
$oldfolder->cache->rename($newname);
}
return $success;
}
/**
* Rename or Create a new IMAP folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array &$prop Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
*
* @return string|false New folder name or False on failure
*
* @see self::set_folder_props() for list of other properties
*/
public static function folder_update(&$prop)
{
self::setup();
$folder = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$delimiter = self::$imap->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = self::$imap->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
self::$last_error = 'cannotbeempty';
return false;
}
else if (strlen($folder) > 128) {
self::$last_error = 'nametoolong';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $char) !== false) {
self::$last_error = 'forbiddencharacter';
return false;
}
}
}
- if (!empty($options) && ($options['protected'] || $options['norename'])) {
+ if (!empty($options) && (!empty($options['protected']) || !empty($options['norename']))) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = self::$imap->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = self::$imap->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
self::$last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
$result = self::folder_rename($oldfolder, $folder);
}
else {
$result = true;
}
}
// create new folder
else {
$result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']);
}
if ($result) {
self::set_folder_props($folder, $prop);
}
return $result ? $folder : false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* with kolab_custom_display_names support.
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
// find custom display name in folder METADATA
if ($name = self::custom_displayname($folder)) {
return $name;
}
return self::object_prettyname($folder, $folder_ns);
}
/**
* Get custom display name (saved in metadata) for the given folder
*/
public static function custom_displayname($folder)
{
static $_metadata;
// find custom display name in folder METADATA
if (self::$config->get('kolab_custom_display_names', true) && self::setup()) {
if ($_metadata !== null) {
$metadata = $_metadata;
}
else {
// For performance reasons ask for all folders, it will be cached as one cache entry
$metadata = self::$imap->get_metadata("*", [self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED]);
// If cache is disabled store result in memory
if (!self::$config->get('imap_cache')) {
$_metadata = $metadata;
}
}
if ($data = $metadata[$folder] ?? null) {
if (($name = $data[self::NAME_KEY_PRIVATE]) || ($name = $data[self::NAME_KEY_SHARED])) {
return $name;
}
}
}
return false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_prettyname($folder, &$folder_ns = null)
{
self::setup();
$found = false;
$namespace = self::$imap->get_namespace();
$prefix = null;
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;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix and extract username
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username part and map it to user name
$pos = strpos($folder, $delim);
$fid = $pos ? substr($folder, 0, $pos) : $folder;
if ($user = self::folder_id2user($fid, true)) {
$fid = str_replace($delim, '', $user);
}
$prefix = "($fid)";
$folder = $pos ? substr($folder, $pos + 1) : '';
$found = true;
$folder_ns = '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;
break;
}
}
}
if (empty($delim)) {
$delim = self::$imap->get_hierarchy_delimiter();
}
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$folder = html::quote($folder);
$folder = str_replace(html::quote($delim), ' &raquo; ', $folder);
if ($prefix) {
$folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
}
if (empty($folder_ns)) {
$folder_ns = 'personal';
}
return $folder;
}
/**
* Helper method to generate a truncated folder name to display.
* Note: $origname is a string returned by self::object_name()
*/
public static function folder_displayname($origname, &$names)
{
$name = $origname;
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i] . ' &raquo; ') === 0) {
$length = strlen($names[$i] . ' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$diff = 1;
// check if prefix folder is in other users namespace
for ($n = count($names)-1; $n >= 0; $n--) {
if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
$diff = 0;
break;
}
}
$name = str_repeat('&nbsp;&nbsp;&nbsp;', $count - $diff) . '&raquo; ' . substr($name, $length);
break;
}
// other users namespace and parent folder exists
else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
$length = strlen('(' . $names[$i] . ') ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;&nbsp;', $count) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
return $name;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
// get all folders of specified type (sorted)
$folders = self::get_folders($type, true);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
// skip current folder and it's subfolders
if ($len) {
if ($name == $current) {
// Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
if ($p_len && !isset($names[$parent])) {
$names[$parent] = self::object_name($parent);
}
continue;
}
if (strpos($name, $current.$delim) === 0) {
continue;
}
}
// always show the parent of current folder
if ($p_len && $name == $parent) {
}
// skip folders where user have no rights to create subfolders
else if ($c_folder->get_owner() != $_SESSION['username']) {
$rights = $c_folder->get_myrights();
if (!preg_match('/[ck]/', $rights)) {
continue;
}
}
$names[$name] = $c_folder->get_name();
}
// Build SELECT field of parent folder
$attrs['is_escaped'] = true;
$select = new html_select($attrs);
$select->add('---', '');
$listnames = array();
foreach (array_keys($names) as $imap_name) {
$name = $origname = $names[$imap_name];
// find folder prefix to truncate
for ($i = count($listnames)-1; $i >= 0; $i--) {
if (strpos($name, $listnames[$i].' &raquo; ') === 0) {
$length = strlen($listnames[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$listnames[] = $origname;
$select->add($name, $imap_name);
}
return $select;
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param bool Enable to 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_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
if (!self::setup()) {
return null;
}
// use IMAP subscriptions
if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
$subscribed = true;
}
if (!$filter) {
// Get ALL folders list, standard way
if ($subscribed) {
$folders = self::_imap_list_subscribed($root, $mbox, $filter);
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
return $folders;
}
$prefix = $root . $mbox;
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
// get folders types for all folders
$folderdata = self::folders_typedata($prefix);
if (!is_array($folderdata)) {
return array();
}
// If we only want groupware folders and don't care about the subscription state,
// then the metadata will already contain all folder names and we can avoid the LIST below.
if (!$subscribed && $filter != 'mail' && $prefix == '*') {
foreach ($folderdata as $folder => $type) {
if (!preg_match($regexp, (string) $type)) {
unset($folderdata[$folder]);
}
}
return self::$imap->sort_folder_list(array_keys($folderdata), true);
}
// Get folders list
if ($subscribed) {
$folders = self::_imap_list_subscribed($root, $mbox, $filter);
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
// In case of an error, return empty list (?)
if (!is_array($folders)) {
return array();
}
// Filter folders list
foreach ($folders as $idx => $folder) {
$type = $folderdata[$folder] ?? null;
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($folders[$idx]);
}
}
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;
}
/**
* Wrapper for rcube_imap::list_folders_subscribed()
* with support for temporarily subscribed folders
*/
protected static function _imap_list_subscribed($root, $mbox, $filter = null)
{
$folders = self::$imap->list_folders_subscribed($root, $mbox);
// add temporarily subscribed folders
if ($filter != 'mail' && self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'] ?? null)) {
$folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_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();
$query = str_replace('*', '', $query);
// 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 (($query == '' || 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, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Sort the given list of kolab folders by namespace/name
*
* @param array List of kolab_storage_folder objects
* @return array Sorted list of folders
*/
public static function sort_folders($folders)
{
$pad = ' ';
$out = array();
$nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
foreach ($folders as $folder) {
$_folders[$folder->name] = $folder;
$ns = $folder->get_namespace();
$nsnames[$ns][$folder->name] = strtolower(html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode &raquo;
}
// $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);
foreach (array_keys($nsnames[$ns]) as $utf7name) {
$out[] = $_folders[$utf7name];
}
}
return $out;
}
/**
* 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 Flat folders list
*/
public static function folder_hierarchy($folders, &$tree = null)
{
if (!self::setup()) {
return array();
}
$_folders = array();
$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)) {
$tree->children[] = $folder;
}
else {
$parents = array();
$depth = $folder->get_namespace() == 'personal' ? 1 : 2;
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, $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);
$refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
}
$parents[] = $refs[$parent];
}
}
if (!empty($parents)) {
$parents = array_reverse($parents);
foreach ($parents as $parent) {
- $parent_node = $refs[$parent->parent] ?: $tree;
+ $parent_node = !empty($refs[$parent->parent]) ? $refs[$parent->parent] : $tree;
$parent_node->children[] = $parent;
$_folders[] = $parent;
}
}
- $parent_node = $refs[$folder->parent] ?: $tree;
+ $parent_node = !empty($refs[$folder->parent]) ? $refs[$folder->parent] : $tree;
$parent_node->children[] = $folder;
}
$refs[$folder->name] = $folder;
$_folders[] = $folder;
unset($folders[$idx]);
}
return $_folders;
}
/**
* Returns folder types indexed by folder name
*
* @param string $prefix Folder prefix (Default '*' for all folders)
*
* @return array|bool List of folders, False on failure
*/
public static function folders_typedata($prefix = '*')
{
if (!self::setup()) {
return false;
}
$type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
// fetch metadata from *some* folders only
if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
$delimiter = self::$imap->get_hierarchy_delimiter();
$folderdata = $blacklist = array();
foreach ((array)$skip_ns as $ns) {
if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
$blacklist[] = $ns_root;
}
}
foreach (array('personal','other','shared') as $ns) {
if (!in_array($ns, (array)$skip_ns)) {
$ns_root = rtrim(self::namespace_root($ns), $delimiter);
// list top-level folders and their childs one by one
// GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
if ($ns_root == '') {
foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
if (!in_array($folder, $blacklist)) {
$folderdata[$folder] = $metadata;
$opts = self::$imap->folder_attributes($folder);
if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
$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);
}
/**
* Callback for array_map to select the correct annotation value
*/
public static function folder_select_metadata($types)
{
if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
return $types[self::CTYPE_KEY_PRIVATE];
}
else if (!empty($types[self::CTYPE_KEY])) {
list($ctype, ) = explode('.', $types[self::CTYPE_KEY]);
return $ctype;
}
return null;
}
/**
* Returns type of IMAP folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
public static function folder_type($folder)
{
self::setup();
$metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($metadata)) {
return null;
}
if (!empty($metadata[$folder])) {
return self::folder_select_metadata($metadata[$folder]);
}
return 'mail';
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return bool True on success
*/
public static function set_folder_type($folder, $type = 'mail')
{
self::setup();
list($ctype, $subtype) = strpos($type, '.') !== false ? explode('.', $type) : array($type, null);
$success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
if (!$success) {
// fallback: only set private annotation
$success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
}
return $success;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
* @param bool $temp Include temporary/session subscriptions
*
* @return bool True if subscribed, false if not
*/
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) ||
($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param bool $temp Only subscribe temporarily for the current session
*
* @return bool True on success, false on error
*/
public static function folder_subscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (self::folder_is_subscribed($folder)) {
return true;
}
else if (empty($_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;
}
return false;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param bool $temp Only remove temporary subscription
*
* @return bool True on success, false on error
*/
public static function folder_unsubscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (!empty($_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;
}
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return bool True if active, false if not
*/
public static function folder_is_active($folder)
{
$active_folders = self::get_states();
return in_array($folder, $active_folders);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return bool True on success, false on error
*/
public static function folder_activate($folder)
{
// activation implies temporary subscription
self::folder_subscribe($folder, true);
return self::set_state($folder, true);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return bool True on success, false on error
*/
public static function folder_deactivate($folder)
{
// remove from temp subscriptions, really?
self::folder_unsubscribe($folder, true);
return self::set_state($folder, false);
}
/**
* Return list of active folders
*/
private static function get_states()
{
if (self::$states !== null) {
return self::$states;
}
$rcube = rcube::get_instance();
$folders = $rcube->config->get('kolab_active_folders');
if ($folders !== null) {
self::$states = !empty($folders) ? explode('**', $folders) : array();
}
// for backward-compatibility copy server-side subscriptions to activation states
else {
self::setup();
if (self::$subscriptions === null) {
self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
self::$with_tempsubs = true;
}
self::$states = (array) self::$subscriptions;
$folders = implode('**', self::$states);
$rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
return self::$states;
}
/**
* Update list of active folders
*/
private static function set_state($folder, $state)
{
self::get_states();
// update in-memory list
$idx = array_search($folder, self::$states);
if ($state && $idx === false) {
self::$states[] = $folder;
}
else if (!$state && $idx !== false) {
unset(self::$states[$idx]);
}
// update user preferences
$folders = implode('**', self::$states);
return rcube::get_instance()->user->save_prefs(array('kolab_active_folders' => $folders));
}
/**
* Creates default folder of specified type
* To be run when none of subscribed folders (of specified type) is found
*
* @param string $type Folder type
* @param string $props Folder properties (color, etc)
*
* @return string Folder name
*/
public static function create_default_folder($type, $props = array())
{
if (!self::setup()) {
return;
}
$folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE));
// from kolab_folders config
$folder_type = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default';
$default_name = self::$config->get('kolab_folders_' . $folder_type);
$folder_type = str_replace('_', '.', $folder_type);
// check if we have any folder in personal namespace
// folder(s) may exist but not subscribed
foreach ((array)$folders as $f => $data) {
if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
$folder = $f;
break;
}
}
if (!$folder) {
if (!$default_name) {
$default_name = self::$default_folders[$type];
}
if (!$default_name) {
return;
}
$folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP');
$prefix = self::$imap->get_namespace('prefix');
// add personal namespace prefix if needed
if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
if (!self::$imap->folder_exists($folder)) {
if (!self::$imap->create_folder($folder)) {
return;
}
}
self::set_folder_type($folder, $folder_type);
}
self::folder_subscribe($folder);
if ($props['active']) {
self::set_state($folder, true);
}
if (!empty($props)) {
self::set_folder_props($folder, $props);
}
return $folder;
}
/**
* Sets folder metadata properties
*
* @param string $folder Folder name
* @param array &$prop Folder properties (color, displayname)
*/
public static function set_folder_props($folder, &$prop)
{
if (!self::setup()) {
return;
}
// TODO: also save 'showalarams' and other properties here
$ns = self::$imap->folder_namespace($folder);
$supported = array(
'color' => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE),
'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE),
);
foreach ($supported as $key => $metakeys) {
if (array_key_exists($key, $prop)) {
$meta_saved = false;
if ($ns == 'personal') // save in shared namespace for personal folders
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
if (!$meta_saved) // try in private namespace
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
if ($meta_saved)
unset($prop[$key]); // unsetting will prevent fallback to local user prefs
}
}
}
/**
* Search users in Kolab LDAP 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 of users
*/
public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
{
$query = str_replace('*', '', $query);
// requires a working LDAP setup
if (!strlen($query) || !($ldap = self::ldap())) {
return array();
}
$root = self::namespace_root('other');
$user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
$search_attrib = self::$config->get('kolab_users_search_attrib', array('cn','mail','alias'));
// search users using the configured attributes
$results = $ldap->dosearch($search_attrib, $query, $mode, $required, $limit, $count);
// exclude myself
if ($_SESSION['kolab_dn']) {
unset($results[$_SESSION['kolab_dn']]);
}
// resolve to IMAP folder name
array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
list($localpart, ) = 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 int 1 - subscribed folders only, 0 - all folders, 2 - all non-active
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_user_folders($user, $type, $subscribed = 0, &$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');
$prefix = $other_ns . $mbox . $delimiter;
$subscribed = (int) $subscribed;
$subs = $subscribed < 2 ? (bool) $subscribed : false;
$folders = self::list_folders($prefix, '*', $type, $subs, $folderdata);
if ($subscribed === 2 && !empty($folders)) {
$active = self::get_states();
if (!empty($active)) {
$folders = array_diff($folders, $active);
}
}
}
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 bool 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 (empty($folders[$foldername])) {
$folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
}
}
// for every (subscribed) user folder, list all (unsubscribed) subfolders
foreach ($folders as $userfolder) {
foreach ((array) self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) {
if (empty($folders[$foldername])) {
$folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername] ?? null);
$userfolder->children[] = $folders[$foldername];
}
}
}
}
return $folders;
}
/**
* Handler for user_delete plugin hooks
*
* Remove all cache data from the local database related to the given user.
*/
public static function delete_user_folders($args)
{
$db = rcmail::get_instance()->get_dbh();
$prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
$db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix);
}
/**
* Get folder METADATA for all supported keys
* Do this in one go for better caching performance
*/
public static function folder_metadata($folder)
{
if (self::setup()) {
$keys = array(
// For better performance we skip displayname here, see (self::custom_displayname())
// self::NAME_KEY_PRIVATE,
// self::NAME_KEY_SHARED,
self::CTYPE_KEY,
self::CTYPE_KEY_PRIVATE,
self::COLOR_KEY_PRIVATE,
self::COLOR_KEY_SHARED,
self::UID_KEY_SHARED,
self::UID_KEY_CYRUS,
);
$metadata = self::$imap->get_metadata($folder, $keys);
return $metadata[$folder];
}
}
/**
* Get user attributes for specified other user (imap) folder identifier.
*
* @param string $folder_id Folder name w/o path (imap user identifier)
* @param bool $as_string Return configured display name attribute value
*
* @return array User attributes
* @see self::ldap()
*/
public static function folder_id2user($folder_id, $as_string = false)
{
static $domain, $cache, $name_attr;
$rcube = rcube::get_instance();
if ($domain === null) {
list(, $domain) = explode('@', $rcube->get_user_name());
}
if ($name_attr === null) {
$name_attr = (array) ($rcube->config->get('kolab_users_name_field', $rcube->config->get('kolab_auth_name')) ?: 'name');
}
$token = $folder_id;
if ($domain && strpos($token, '@') === false) {
$token .= '@' . $domain;
}
if ($cache === null) {
$cache = $rcube->get_cache_shared('kolab_users') ?: false;
}
// use value cached in memory for repeated lookups
if (!$cache && array_key_exists($token, self::$ldapcache)) {
$user = self::$ldapcache[$token];
}
if (empty($user) && $cache) {
$user = $cache->get($token);
}
if (empty($user) && ($ldap = self::ldap())) {
$user = $ldap->get_user_record($token, $_SESSION['imap_host']);
if (!empty($user)) {
$keys = array('displayname', 'name', 'mail'); // supported keys
$user = array_intersect_key($user, array_flip($keys));
if (!empty($user)) {
if ($cache) {
$cache->set($token, $user);
}
else {
self::$ldapcache[$token] = $user;
}
}
}
}
if (!empty($user)) {
if ($as_string) {
foreach ($name_attr as $attr) {
if ($display = $user[$attr]) {
break;
}
}
if (!$display) {
$display = $user['displayname'] ?: $user['name'];
}
if ($display && $display != $folder_id) {
$display = "$display ($folder_id)";
}
return $display;
}
return $user;
}
}
/**
* Chwala's 'folder_mod' hook handler for mapping other users folder names
*/
public static function folder_mod($args)
{
static $roots;
if ($roots === null) {
self::setup();
$roots = self::$imap->get_namespace('other');
}
// Note: We're working with UTF7-IMAP encoding here
if ($args['dir'] == 'in') {
foreach ((array) $roots as $root) {
if (strpos($args['folder'], $root[0]) === 0) {
// remove root and explode folder
$delim = $root[1];
$folder = explode($delim, substr($args['folder'], strlen($root[0])));
// compare first (user) part with a regexp, it's supposed
// to look like this: "Doe, Jane (uid)", so we can extract the uid
// and replace the folder with it
if (preg_match('~^[^/]+ \(([^)]+)\)$~', $folder[0], $m)) {
$folder[0] = $m[1];
$args['folder'] = $root[0] . implode($delim, $folder);
}
break;
}
}
}
else { // dir == 'out'
foreach ((array) $roots as $root) {
if (strpos($args['folder'], $root[0]) === 0) {
// remove root and explode folder
$delim = $root[1];
$folder = explode($delim, substr($args['folder'], strlen($root[0])));
// Replace uid with "Doe, Jane (uid)"
if ($user = self::folder_id2user($folder[0], true)) {
$user = str_replace($delim, '', $user);
$folder[0] = rcube_charset::convert($user, RCUBE_CHARSET, 'UTF7-IMAP');
$args['folder'] = $root[0] . implode($delim, $folder);
}
break;
}
}
}
return $args;
}
}
diff --git a/plugins/libkolab/lib/kolab_utils.php b/plugins/libkolab/lib/kolab_utils.php
index 571bf286..d80d0140 100644
--- a/plugins/libkolab/lib/kolab_utils.php
+++ b/plugins/libkolab/lib/kolab_utils.php
@@ -1,93 +1,97 @@
<?php
/**
* Utility class providing unified functionality for other plugins.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-2018, 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_utils
{
public static function folder_form($form, $folder, $domain, $hidden_fields = array(), $no_acl = false)
{
$rcmail = rcmail::get_instance();
// add folder ACL tab
if (!$no_acl && is_string($folder) && strlen($folder)) {
$form['sharing'] = array(
'name' => rcube::Q($rcmail->gettext('libkolab.tabsharing')),
'content' => self::folder_acl_form($folder),
);
}
$form_html = '';
if (is_array($hidden_fields)) {
foreach ($hidden_fields as $field) {
$hiddenfield = new html_hiddenfield($field);
$form_html .= $hiddenfield->show() . "\n";
}
}
// create form output
foreach ($form as $tab) {
- if (is_array($tab['fields']) && empty($tab['content'])) {
+ if (isset($tab['fields']) && is_array($tab['fields']) && empty($tab['content'])) {
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
foreach ($tab['fields'] as $col => $colprop) {
$label = !empty($colprop['label']) ? $colprop['label'] : $rcmail->gettext("$domain.$col");
$table->add('title', html::label($colprop['id'], rcube::Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show();
}
else {
$content = $tab['content'];
}
if (!empty($content)) {
$form_html .= html::tag('fieldset', null, html::tag('legend', null, rcube::Q($tab['name'])) . $content) . "\n";
}
}
return $form_html;
}
/**
* Handler for ACL form template object
*/
public static function folder_acl_form($folder)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$options = $storage->folder_info($folder);
$rcmail->plugins->load_plugin('acl', true);
// get sharing UI from acl plugin
$acl = $rcmail->plugins->exec_hook('folder_form', array(
'form' => array(),
'options' => $options,
'name' => $folder
));
- return $acl['form']['sharing']['content'] ?: html::div('hint', $rcmail->gettext('libkolab.aclnorights'));
+ if (!empty($acl['form']['sharing']['content'])) {
+ return $acl['form']['sharing']['content'];
+ }
+
+ return html::div('hint', $rcmail->gettext('libkolab.aclnorights'));
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Jan 18, 8:59 PM (10 h, 51 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
120011
Default Alt Text
(127 KB)

Event Timeline