diff --git a/lib/Auth/LDAP.php b/lib/Auth/LDAP.php
index 4bf2a8f..aac6df8 100644
--- a/lib/Auth/LDAP.php
+++ b/lib/Auth/LDAP.php
@@ -1,1495 +1,1500 @@
 <?php
 /*
  +--------------------------------------------------------------------------+
  | This file is part of the Kolab Web Admin Panel                           |
  |                                                                          |
  | Copyright (C) 2011-2012, Kolab Systems AG                                |
  |                                                                          |
  | 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/>      |
  +--------------------------------------------------------------------------+
  | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
  | Author: Jeroen van Meeuwen <vanmeeuwen@kolabsys.com>                     |
  +--------------------------------------------------------------------------+
 */
 
 /**
  * Kolab LDAP handling abstraction class.
  */
 class LDAP extends Net_LDAP3 {
     private $conf;
 
     /**
      * Class constructor
      */
     public function __construct($domain = null)
     {
         parent::__construct();
 
         $this->conf = Conf::get_instance();
 
         $this->config_set("debug", Log::mode() == Log::TRACE);
         $this->config_set("log_hook", array($this, "_log"));
 
         $this->config_set("config_root_dn", "cn=config");
         $this->config_set("service_bind_dn", $this->conf->get("service_bind_dn"));
         $this->config_set("service_bind_pw", $this->conf->get("service_bind_pw"));
 
         $this->config_set("login_filter", $this->conf->get("kolab_wap", "login_filter"));
         $this->config_set("vlv", $this->conf->get("ldap", "vlv", Conf::AUTO));
 
         // configure the cache
         $memcache_hosts = $this->conf->get('kolab_wap', 'memcache_hosts');
         $this->config_set("memcache_pconnect", $this->conf->get('kolab_wap', 'memcache_pconnect', Conf::BOOL));
         $this->config_set("memcache_hosts", $memcache_hosts);
         $this->config_set("cache", !empty($memcache_hosts));
 
         $this->config_set("domain_base_dn", $this->conf->get('ldap', 'domain_base_dn'));
         $this->config_set("domain_filter", $this->conf->get('ldap', 'domain_filter'));
         $this->config_set("domain_name_attribute", $this->conf->get('ldap', 'domain_name_attribute'));
 
         // See if we are to connect to any domain explicitly defined.
         if (empty($domain)) {
             // If not, attempt to get the domain from the session.
             if (isset($_SESSION['user'])) {
                 try {
                     $domain = $_SESSION['user']->get_domain();
                 } catch (Exception $e) {
                     $this->_log(LOG_WARNING, "LDAP: User not authenticated yet");
                 }
             }
         } else {
             $this->_log(LOG_DEBUG, "LDAP: __construct() using domain $domain");
         }
 
         // Continue and default to the primary domain.
         $this->domain = $domain ? $domain : $this->conf->get('primary_domain');
 
         $unique_attr = $this->conf->get($domain, 'unique_attribute');
 
         if (empty($unique_attr)) {
             $unique_attr = $this->conf->get('ldap', 'unique_attribute');
         }
 
         if (empty($unique_attr)) {
             $unique_attr = 'nsuniqueid';
         }
 
         $this->config_set('unique_attribute', $unique_attr);
 
         $this->_ldap_uri    = $this->conf->get('ldap_uri');
         $this->_ldap_port   = parse_url($this->_ldap_uri, PHP_URL_PORT);
         $this->_ldap_scheme = parse_url($this->_ldap_uri, PHP_URL_SCHEME);
 
         // Catch cases in which the ldap server port has not been explicitely defined
         if (!$this->_ldap_port) {
             if ($this->_ldap_scheme == "ldaps") {
                 $this->_ldap_port = 636;
             }
             else {
                 $this->_ldap_port = 389;
             }
         }
 
         $this->config_set("host", $this->_ldap_uri);
         $this->config_set("port", $this->_ldap_port);
         $this->config_set("use_tls", $this->_ldap_scheme == 'tls');
 
         parent::connect();
 
         // Attempt to get the root dn from the configuration file.
         $root_dn = $this->conf->get($this->domain, "base_dn");
         if (empty($root_dn)) {
             // Fall back to a root dn from LDAP, or the standard root dn
             $root_dn = $this->domain_root_dn($this->domain);
         }
 
         $this->config_set("root_dn", $root_dn);
     }
 
     /**********************************************************
      ***********          Public methods           ************
      **********************************************************/
 
     /**
      * Authentication
      *
      * @param string $username User name (DN or mail)
      * @param string $password User password
      *
      * @return bool|string User ID or False on failure
      */
     public function authenticate($username, $password, $domain = null, &$attributes = null)
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP: authentication request for $username against domain $domain");
 
         if (!$domain) {
             $domain = $this->domain;
         }
 
         $result = $this->login($username, $password, $domain, $attributes);
 
         if (!$result) {
             return false;
         }
 
         $_SESSION['user']->user_bind_dn = $result;
         $_SESSION['user']->user_bind_pw = $password;
 
         return $result;
     }
 
     public function domain_add($domain, $attributes = array())
     {
         if (empty($domain)) {
             return false;
         }
 
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $domain_base_dn        = $this->conf->get('ldap', 'domain_base_dn');
         $domain_name_attribute = $this->conf->get('ldap', 'domain_name_attribute');
         $service_bind_dn       = $this->conf->get('ldap', 'service_bind_dn');
         $primary_domain        = $this->conf->get('kolab', 'primary_domain');
 
 	if (!empty($attributes['domainrelatedobject_only'])) {
             $domainrelatedobject_only = (bool)($attributes['domainrelatedobject_only']);
             unset($attributes['domainrelatedobject_only']);
         } else {
             $domainrelatedobject_only = false;
         }
 
         if (empty($service_bind_dn)) {
             $service_bind_dn = $this->conf->get('ldap', 'bind_dn');
         }
 
         if (empty($domain_name_attribute)) {
             $domain_name_attribute = 'associateddomain';
         }
 
         if (!is_array($attributes[$domain_name_attribute])) {
             $attributes[$domain_name_attribute] = (array) $attributes[$domain_name_attribute];
         }
 
         if (!in_array($domain, $attributes[$domain_name_attribute])) {
             array_unshift($attributes[$domain_name_attribute], $domain);
         }
 
         $domain_dn = $domain_name_attribute . '=' . $domain . ',' . $domain_base_dn;
 
         if (!empty($attributes['inetdomainbasedn'])) {
             $inetdomainbasedn = $attributes['inetdomainbasedn'];
         }
         else {
             $inetdomainbasedn = $this->_standard_root_dn($domain);
         }
 
         if (empty($attributes['aci'])) {
             $attributes['aci'] = array(
                 "(targetattr = \"*\") (version 3.0;acl \"Read Access for {$domain} Users\";allow (read,compare,search)(userdn = \"ldap:///{$inetdomainbasedn}??sub?(objectclass=*)\");)"
             );
         }
 
         $result = $this->add_entry($domain_dn, $attributes);
 
         if (!$result) {
             return false;
         }
 
         // Return if the request specified to only create the domainrelatedobject
         if ($domainrelatedobject_only) {
             return true;
         }
 
         // Query the ACI for the primary domain
         if ($domain_entry = $this->find_domain($primary_domain)) {
             if (in_array('inetdomainbasedn', $domain_entry)) {
                 $_base_dn = $domain_entry['inetdomainbasedn'];
             }
         }
 
         if (empty($_base_dn)) {
             $_base_dn = $this->_standard_root_dn($primary_domain);
         }
 
         $result = $this->_read($_base_dn, array('aci'));
         $result = $result[key($result)];
         $acis   = $result['aci'];
 
         // Skip one particular ACI
         foreach ($acis as $aci) {
             if (stristr($aci, "SIE Group") === false) {
                 continue;
             }
             $_aci = $aci;
         }
 
         // @TODO: this list should be configurable or auto-created somehow
         $self_attrs = array(
             'carLicense', 'description', 'displayName', 'facsimileTelephoneNumber', 'homePhone',
             'homePostalAddress', 'initials', 'jpegPhoto', 'l', 'labeledURI', 'mobile', 'o', 'pager', 'photo',
             'postOfficeBox', 'postalAddress', 'postalCode', 'preferredDeliveryMethod', 'preferredLanguage',
             'registeredAddress', 'roomNumber', 'secretary', 'seeAlso', 'st', 'street', 'telephoneNumber',
             'telexNumber', 'title', 'userCertificate', 'userPassword', 'userSMIMECertificate',
             'x500UniqueIdentifier',
         );
         if (in_array('kolabInetOrgPerson', $this->classes_allowed())) {
              $self_attrs = array_merge($self_attrs, array('kolabDelegate', 'kolabInvitationPolicy', 'kolabAllowSMTPSender'));
         }
 
         $_domain = str_replace('.', '_', $domain);
         $dn      = $inetdomainbasedn;
         $cn      = str_replace(array(',', '='), array('\2C', '\3D'), $dn);
 
         // Additional domain entries in various trees
         $entries = array(
             "cn={$cn},cn=mapping tree,cn=config" => array(
                 'objectclass' => array(
                     'top',
                     'extensibleObject',
                     'nsMappingTree',
                 ),
                 'nsslapd-state'   => 'backend',
                 'cn'              => $inetdomainbasedn,
                 'nsslapd-backend' => $_domain,
             ),
             "cn={$_domain},cn=ldbm database,cn=plugins,cn=config" => array(
                 'objectclass' => array(
                     'top',
                     'extensibleobject',
                     'nsbackendinstance',
                 ),
                 'cn'                     => $_domain,
                 'nsslapd-suffix'         => $inetdomainbasedn,
                 'nsslapd-cachesize'      => '-1',
                 'nsslapd-cachememsize'   => '10485760',
                 'nsslapd-readonly'       => 'off',
                 'nsslapd-require-index'  => 'off',
                 'nsslapd-dncachememsize' => '10485760',
                 'nsslapd-directory'      => true, // will be replaced below
             ),
             $inetdomainbasedn => array(
                 // @TODO: Probably just use ldap_explode_dn()
                 'dc'          => substr($dn, (strpos($dn, '=')+1), ((strpos($dn, ',')-strpos($dn, '='))-1)),
                 'objectclass' => array(
                     'top',
                     'domain',
                 ),
                 'aci' => array(
                     // Self-modification
                     "(targetattr = \"" . implode(" || ", $self_attrs) . "\")(version 3.0; acl \"Enable self write for common attributes\"; allow (read,compare,search,write) userdn=\"ldap:///self\";)",
                     // Directory Administrators
                     "(targetattr = \"*\")(version 3.0; acl \"Directory Administrators Group\"; allow (all) (groupdn=\"ldap:///cn=Directory Administrators,{$inetdomainbasedn}\" or roledn=\"ldap:///cn=kolab-admin,{$inetdomainbasedn}\");)",
                     // Configuration Administrators
                     "(targetattr = \"*\")(version 3.0; acl \"Configuration Administrators Group\"; allow (all) groupdn=\"ldap:///cn=Configuration Administrators,ou=Groups,ou=TopologyManagement,o=NetscapeRoot\";)",
                     // Administrator users
                     "(targetattr = \"*\")(version 3.0; acl \"Configuration Administrator\"; allow (all) userdn=\"ldap:///uid=admin,ou=Administrators,ou=TopologyManagement,o=NetscapeRoot\";)",
                     // SIE Group
                     $_aci,
                     // Search Access,
                     "(targetattr != \"userPassword\") (version 3.0; acl \"Search Access\"; allow (read,compare,search) (userdn = \"ldap:///{$inetdomainbasedn}??sub?(objectclass=*)\");)",
                     // Service Search Access
                     "(targetattr = \"*\") (version 3.0; acl \"Service Search Access\"; allow (read,compare,search) (userdn = \"ldap:///{$service_bind_dn}\");)",
                 ),
             ),
         );
 
         $replica_hosts = $this->list_replicas();
 
         if (!empty($replica_hosts)) {
             foreach ($replica_hosts as $replica_host) {
                 $ldap = new Net_LDAP3($this->config);
                 $ldap->config_set("log_hook", array($this, "_log"));
                 $ldap->config_set('hosts', array($replica_host));
                 $ldap->connect();
                 $ldap->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
                 foreach ($entries as $dn => $attrs) {
                     if (isset($attrs['nsslapd-directory'])) {
                         $attrs['nsslapd-directory'] = $this->nsslapd_directory($ldap, $domain);
                     }
                     if (!$ldap->add_entry($dn, $attrs)) {
                         $this->_log(LOG_ERR, "Error adding $dn to $replica_host");
                     }
                 }
                 $ldap->close();
             }
         }
         else {
             foreach ($entries as $dn => $attrs) {
                 if (isset($attrs['nsslapd-directory'])) {
                     $attrs['nsslapd-directory'] = $this->nsslapd_directory($this, $domain);
                 }
                 if (!$this->add_entry($dn, $attrs)) {
                     $this->_log(LOG_ERR, "Error adding $dn");
                 }
             }
         }
 
         if (!empty($replica_hosts)) {
             $this->add_replication_agreements($inetdomainbasedn);
         }
 
         // add OUs, do this after adding replication agreements
         $entries = array(
             "cn=Directory Administrators,$inetdomainbasedn" => array(
                 'cn'           => 'Directory Administrators',
                 'objectclass'  => array('top', 'groupofuniquenames'),
                 'uniquemember' => array('cn=Directory Manager'),
             ),
             "cn=kolab-admin,$inetdomainbasedn" => array(
                 'cn'          => 'kolab-admin',
                 'objectclass' => array(
                     'top',
                     'ldapsubentry',
                     'nsroledefinition',
                     'nssimpleroledefinition',
                     'nsmanagedroledefinition',
                 ),
             ),
             // @TODO: these OUs DN should be read from config
             "ou=Groups,$inetdomainbasedn" => array(
                 'ou'          => 'Groups',
                 'objectclass' => array('top', 'organizationalunit'),
             ),
             "ou=People,$inetdomainbasedn" => array(
                 'ou'          => 'People',
                 'objectclass' => array('top', 'organizationalunit'),
             ),
             "ou=Special Users,$inetdomainbasedn" => array(
                 'ou'          => 'Special Users',
                 'objectclass' => array('top', 'organizationalunit'),
             ),
             "ou=Resources,$inetdomainbasedn" => array(
                 'ou'          => 'Resources',
                 'objectclass' => array('top', 'organizationalunit'),
             ),
             "ou=Shared Folders,$inetdomainbasedn" => array(
                 'ou'          => 'Shared Folders',
                 'objectclass' => array('top', 'organizationalunit'),
             ),
         );
 
         // create set of OUs and other domain entries
         foreach ($entries as $dn => $attrs) {
             $this->add_entry($dn, $attrs);
         }
 
         return $domain_dn;
     }
 
     public function domain_edit($domain, $attributes, $typeid = null)
     {
         $domain = $this->domain_info($domain, array_keys($attributes));
 
         if (empty($domain)) {
             return false;
         }
 
+        // remove invalid/hidden attributes
+        if (isset($attributes['domainrelatedobject_only'])) {
+            unset($attributes['domainrelatedobject_only']);
+        }
+
         $domain_dn = key($domain);
 
         // We should start throwing stuff over the fence here.
         return $this->modify_entry($domain_dn, $domain[$domain_dn], $attributes);
     }
 
     public function domain_delete($domain)
     {
         $domain = $this->domain_info($domain);
 
         if (empty($domain)) {
             return false;
         }
 
         $domain_dn  = key($domain);
         $attributes = array_merge($domain[$domain_dn], array('inetdomainstatus' => 'deleted'));
 
         // for performance reasons we set only domain status,
         // cronjob script should delete such domain later
         return $this->modify_entry($domain_dn, $domain[$domain_dn], $attributes);
     }
 
     public function domain_find_by_attribute($attribute)
     {
         $base_dn = $this->conf->get('ldap', 'domain_base_dn');
 
         return $this->entry_find_by_attribute($attribute, $base_dn);
     }
 
     public function domain_info($domain, $attributes = array('*'))
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::domain_info() for domain " . var_export($domain, true));
 
         if (empty($domain)) {
             return false;
         }
 
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $domain_base_dn = $this->conf->get('ldap', 'domain_base_dn');
         $domain_dn      = $this->entry_dn($domain, array(), $domain_base_dn);
 
         if (!$domain_dn) {
             if ($result = $this->find_domain($domain, $attributes)) {
                 $result_dn = $result['dn'];
                 unset($result['dn']);
                 $result = array($result_dn => $result);
             }
         }
         else {
             $result = $this->_read($domain_dn, $attributes);
         }
 
         $this->_log(LOG_DEBUG, "Auth::LDAP::domain_info() result: " . var_export($result, true));
 
         return $result ? $result : false;
     }
 
     /**
      * Checkes if specified domain is empty (no users assigned)
      *
      * @param string|array $domain Domain name or domain_info() result
      *
      * @return bool True if domain is empty, False otherwise
      */
     public function domain_is_empty($domain)
     {
         $domain_name_attribute = $this->conf->get('ldap', 'domain_name_attribute');
 
         if (empty($domain_name_attribute)) {
             $domain_name_attribute = 'associateddomain';
         }
 
         if (!is_array($domain)) {
             $domain = $this->domain_info($domain);
         }
 
         if (!empty($domain)) {
             $domain_dn   = key($domain);
             $domain_name = $domain[$domain_dn][$domain_name_attribute];
 
             if (is_array($domain_name)) {
                 $domain_name = $domain_name[0];
             }
         }
         else {
             return false;
         }
 
         $result = $this->list_users(array('entrydn'), null, array('page_size' => 1), $domain_name);
 
         return is_array($result) && $result['count'] == 0;
     }
 
     /**
      * Proxy to parent function in order to enable us to insert our
      * configuration.
      */
     public function effective_rights($subject)
     {
         $ckey  = $_SESSION['user']->user_bind_dn . '#'
             . md5($this->domain . '::' . $subject . '::' . $_SESSION['user']->user_bind_pw);
 
         // use memcache
         if ($result = $this->get_cache_data($ckey)) {
             return $result;
         }
         // use internal cache
         else if (isset($this->icache[$ckey])) {
             return $this->icache[$ckey];
         }
 
         // Ensure we are bound with the user's credentials
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $this->_log(LOG_DEBUG, "Auth::LDAP::effective_rights(\$subject = '" . $subject . "')");
 
         switch ($subject) {
             case "domain":
                 $result = parent::effective_rights($this->conf->get("ldap", "domain_base_dn"));
                 break;
 
             case "group":
             case "ou":
             case "resource":
             case "role":
             case "sharedfolder":
             case "user":
                 $result = parent::effective_rights($this->_subject_base_dn($subject));
                 break;
 
             default:
                 $result = parent::effective_rights($subject);
         }
 
         if (!$result) {
             $result = $this->legacy_rights($subject);
         }
 
         if (!$this->set_cache_data($ckey, $result)) {
              $this->icache[$ckey] = $result;
         }
 
         return $result;
     }
 
     public function find_recipient($address)
     {
         if (strpos($address, '@') === false) {
             return false;
         }
 
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $root_dn    = $this->config_get('root_dn');
         $mail_attrs = $this->conf->get_list('mail_attributes') ?: array('mail', 'alias');
         $search     = array('operator' => 'OR');
 
         foreach ($mail_attrs as $num => $attr) {
             $search['params'][$attr] = array(
                 'type'  => 'exact',
                 'value' => $address,
             );
         }
 
         $result = $this->search_entries($root_dn, '(objectclass=*)', 'sub', $mail_attrs,
             array('search' => $search));
 
         if ($result && $result->count() > 0) {
             return $result->entries(true);
         }
 
         return false;
     }
 
     public function get_attributes($subject_dn, $attributes)
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::get_attributes() for $subject_dn");
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         return $this->get_entry_attributes($subject_dn, $attributes);
     }
 
     public function group_add($attrs, $typeid = null)
     {
         $base_dn = $this->entry_base_dn('group', $typeid, $attrs);
 
         // TODO: The rdn is configurable as well.
         // Use [$type_str . "_"]user_rdn_attr
         $dn = "cn=" . Net_LDAP3::quote_string($attrs['cn'], true) . "," . $base_dn;
 
         return $this->entry_add($dn, $attrs);
     }
 
     public function group_delete($group)
     {
         return $this->entry_delete($group);
     }
 
     public function group_edit($group, $attributes, $typeid = null)
     {
         $group = $this->group_info($group, array_keys($attributes));
 
         if (empty($group)) {
             return false;
         }
 
         $group_dn = key($group);
 
         // We should start throwing stuff over the fence here.
         return $this->modify_entry($group_dn, $group[$group_dn], $attributes);
     }
 
     public function group_find_by_attribute($attribute)
     {
         return $this->entry_find_by_attribute($attribute);
     }
 
     public function group_info($group, $attributes = array('*'))
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::group_info() for group " . var_export($group, true));
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $group_dn = $this->entry_dn($group);
 
         if (!$group_dn) {
             return false;
         }
 
         $this->read_prepare($attributes);
 
         return $this->_read($group_dn, $attributes);
     }
 
     public function group_members_list($group, $recurse = true)
     {
         $group_dn = $this->entry_dn($group);
 
         if (!$group_dn) {
             return false;
         }
 
         return $this->list_group_members($group_dn, null, $recurse);
     }
 
     public function list_domains($attributes = array(), $search = array(), $params = array())
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::list_domains(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true));
 
         $section = $this->conf->get('kolab', 'auth_mechanism');
         $base_dn = $this->conf->get($section, 'domain_base_dn');
         $filter  = $this->conf->get($section, 'domain_filter');
 
         $kolab_filter = $this->conf->get($section, 'kolab_domain_filter');
         if (empty($filter) && !empty($kolab_filter)) {
             $filter = $kolab_filter;
         }
 
         if (!$filter) {
             $filter = "(associateddomain=*)";
         }
 
         return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params);
     }
 
     public function list_groups($attributes = array(), $search = array(), $params = array())
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::list_groups(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true));
 
         $base_dn = $this->_subject_base_dn('group');
         $filter  = $this->conf->get('group_filter');
 
         if (!$filter) {
             $filter = "(|(objectclass=groupofuniquenames)(objectclass=groupofurls))";
         }
 
         return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params);
     }
 
     public function list_organizationalunits($attributes = array(), $search = array(), $params = array())
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::list_organizationalunits(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true));
 
         $base_dn = $this->_subject_base_dn($params['type'] ? $params['type'] . '_ou' : 'ou');
         $filter  = $this->conf->get('ou_filter');
 
         if (!$filter) {
             $filter = "(objectclass=organizationalunit)";
         }
 
         return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params);
     }
 
     public function list_resources($attributes = array(), $search = array(), $params = array())
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::list_resources(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true));
 
         $base_dn = $this->_subject_base_dn('resource');
         $filter  = $this->conf->get('resource_filter');
 
         if (!$filter) {
             $filter = "(&(objectclass=*)(!(objectclass=organizationalunit)))";
         }
 
         return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params);
     }
 
     public function list_roles($attributes = array(), $search = array(), $params = array())
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::list_roles(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true));
 
         $base_dn = $this->_subject_base_dn('role');
         $filter  = $this->conf->get('role_filter');
 
         if (empty($filter)) {
             $filter  = "(&(objectclass=ldapsubentry)(objectclass=nsroledefinition))";
         }
 
         return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params);
     }
 
     public function list_sharedfolders($attributes = array(), $search = array(), $params = array())
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::list_sharedfolders(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true));
 
         $base_dn = $this->_subject_base_dn('sharedfolder');
         $filter  = $this->conf->get('sharedfolder_filter');
 
         if (!$filter) {
             $filter = "(&(objectclass=*)(!(objectclass=organizationalunit)))";
         }
 
         return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params);
     }
 
     public function list_users($attributes = array(), $search = array(), $params = array(), $domain = null)
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::list_users(" . var_export($attributes, true) . ", " . var_export($search, true) . ", " . var_export($params, true) . ", " . $domain . ")");
 
         $base_dn = $this->_subject_base_dn('user', false, $domain);
         $filter  = $this->conf->get('user_filter');
 
         if (empty($filter)) {
             $filter  = "(objectclass=kolabinetorgperson)";
         }
 
         return $this->_list($base_dn, $filter, 'sub', $attributes, $search, $params);
     }
 
     public function organizationalunit_add($attrs, $typeid = null)
     {
         $base_dn = $this->entry_base_dn('ou', $typeid, $attrs);
 
         // TODO: The rdn is configurable as well.
         // Use [$type_str . "_"]ou_rdn_attr
         $dn = "ou=" . Net_LDAP3::quote_string($attrs['ou'], true) . "," . $base_dn;
 
         return $this->entry_add($dn, $attrs);
     }
 
     public function organizationalunit_edit($ou, $attributes, $typeid = null)
     {
         $ou = $this->organizationalunit_info($ou, array_keys($attributes));
 
         if (empty($ou)) {
             return false;
         }
 
         $dn = key($ou);
 
         // We should start throwing stuff over the fence here.
         return $this->modify_entry($dn, $ou[$dn], $attributes);
     }
 
     public function organizationalunit_delete($ou)
     {
         return $this->entry_delete($ou, array('objectclass' => 'organizationalunit'));
     }
 
     public function organizationalunit_find_by_attribute($attribute)
     {
         $attribute['objectclass'] = 'organizationalunit';
         return $this->entry_find_by_attribute($attribute);
     }
 
     public function organizationalunit_info($ou, $attributes = array('*'))
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::organizationalunit_info() for unit " . var_export($ou, true));
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $dn = $this->entry_dn($ou, array('objectclass' => 'organizationalunit'));
 
         if (!$dn) {
             return false;
         }
 
         $this->read_prepare($attributes);
 
         return $this->_read($dn, $attributes);
     }
 
     public function resource_add($attrs, $typeid = null)
     {
         $base_dn = $this->entry_base_dn('resource', $typeid, $attrs);
 
         // TODO: The rdn is configurable as well.
         // Use [$type_str . "_"]resource_rdn_attr
         $dn = "cn=" . Net_LDAP3::quote_string($attrs['cn'], true) . "," . $base_dn;
 
         return $this->entry_add($dn, $attrs);
     }
 
     public function resource_delete($resource)
     {
         return $this->entry_delete($resource);
     }
 
     public function resource_edit($resource, $attributes, $typeid = null)
     {
         $resource = $this->resource_info($resource, array_keys($attributes));
 
         if (empty($resource)) {
             return false;
         }
 
         $resource_dn = key($resource);
 
         // We should start throwing stuff over the fence here.
         return $this->modify_entry($resource_dn, $resource[$resource_dn], $attributes);
     }
 
     public function resource_find_by_attribute($attribute)
     {
         return $this->entry_find_by_attribute($attribute);
     }
 
     public function resource_info($resource, $attributes = array('*'))
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::resource_info() for resource " . var_export($resource, true));
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $resource_dn = $this->entry_dn($resource);
 
         if (!$resource_dn) {
             return false;
         }
 
         $this->read_prepare($attributes);
 
         return $this->_read($resource_dn, $attributes);
     }
 
     public function role_add($attrs, $typeid = null)
     {
         $base_dn = $this->entry_base_dn('role', $typeid, $attrs);
 
         // TODO: The rdn is configurable as well.
         // Use [$type_str . "_"]role_rdn_attr
         $dn = "cn=" . Net_LDAP3::quote_string($attrs['cn'], true) . "," . $base_dn;
 
         return $this->entry_add($dn, $attrs);
     }
 
     public function role_edit($role, $attributes, $typeid = null)
     {
         $role = $this->role_info($role, array_keys($attributes));
 
         if (empty($role)) {
             return false;
         }
 
         $role_dn = key($role);
 
         // We should start throwing stuff over the fence here.
         return $this->modify_entry($role_dn, $role[$role_dn], $attributes);
     }
 
     public function role_delete($role)
     {
         return $this->entry_delete($role, array('objectclass' => 'ldapsubentry'));
     }
 
     public function role_find_by_attribute($attribute)
     {
         $attribute['objectclass'] = 'ldapsubentry';
         return $this->entry_find_by_attribute($attribute);
     }
 
     public function role_info($role, $attributes = array('*'))
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::role_info() for role " . var_export($role, true));
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $role_dn = $this->entry_dn($role, array('objectclass' => 'ldapsubentry'));
 
         if (!$role_dn) {
             return false;
         }
 
         $this->read_prepare($attributes);
 
         return $this->_read($role_dn, $attributes);
     }
 
     public function sharedfolder_add($attrs, $typeid = null)
     {
         $base_dn = $this->entry_base_dn('sharedfolder', $typeid, $attrs);
 
         // TODO: The rdn is configurable as well.
         // Use [$type_str . "_"]user_rdn_attr
         $dn = "cn=" . Net_LDAP3::quote_string($attrs['cn'], true) . "," . $base_dn;
 
         return $this->entry_add($dn, $attrs);
     }
 
     public function sharedfolder_delete($sharedfolder)
     {
         return $this->entry_delete($sharedfolder);
     }
 
     public function sharedfolder_edit($sharedfolder, $attributes, $typeid = null)
     {
         $sharedfolder = $this->sharedfolder_info($sharedfolder, array_keys($attributes));
 
         if (empty($sharedfolder)) {
             return false;
         }
 
         $sharedfolder_dn = key($sharedfolder);
 
         // We should start throwing stuff over the fence here.
         return $this->modify_entry($sharedfolder_dn, $sharedfolder[$sharedfolder_dn], $attributes);
     }
 
     public function sharedfolder_find_by_attribute($attribute)
     {
         return $this->entry_find_by_attribute($attribute);
     }
 
     public function sharedfolder_info($sharedfolder, $attributes = array('*'))
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::sharedfolder_info() for sharedfolder " . var_export($sharedfolder, true));
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $sharedfolder_dn = $this->entry_dn($sharedfolder);
 
         if (!$sharedfolder_dn) {
             return false;
         }
 
         $this->read_prepare($attributes);
 
         return $this->_read($sharedfolder_dn, $attributes);
     }
 
     public function search($base_dn, $filter = '(objectclass=*)', $scope = 'sub', $attributes = null, $props = array(), $count_only = false)
     {
         if (isset($_SESSION['user']->user_bind_dn) && !empty($_SESSION['user']->user_bind_dn)) {
             $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
         }
 
         return parent::search($base_dn, $filter, $scope, $attributes ?: array('*'), $props, $count_only);
     }
 
     public function subject_base_dn($subject, $strict = false)
     {
         return $this->_subject_base_dn($subject, $strict);
     }
 
     public function user_add($attrs, $typeid = null)
     {
         $base_dn = $this->entry_base_dn('user', $typeid, $attrs);
 
         // TODO: The rdn is configurable as well.
         // Use [$type_str . "_"]user_rdn_attr
         $dn = "uid=" . Net_LDAP3::quote_string($attrs['uid'], true) . "," . $base_dn;
 
         return $this->entry_add($dn, $attrs);
     }
 
     public function user_edit($user, $attributes, $typeid = null)
     {
         $user = $this->user_info($user, array_keys($attributes));
 
         if (empty($user)) {
             return false;
         }
 
         $user_dn = key($user);
 
         // We should start throwing stuff over the fence here.
         $result = $this->modify_entry($user_dn, $user[$user_dn], $attributes);
 
         // Handle modification of current user data
         if (!empty($result) && $user_dn == $_SESSION['user']->user_bind_dn) {
             // update session password
             if (!empty($result['replace']) && !empty($result['replace']['userpassword'])) {
                 $pass = $result['replace']['userpassword'];
                 $_SESSION['user']->user_bind_pw = is_array($pass) ? implode($pass) : $pass;
             }
         }
 
         return $result;
     }
 
     public function user_delete($user)
     {
         return $this->entry_delete($user);
     }
 
     public function user_info($user, $attributes = array('*'))
     {
         $this->_log(LOG_DEBUG, "Auth::LDAP::user_info() for user " . var_export($user, true));
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $user_dn = $this->entry_dn($user);
 
         if (!$user_dn) {
             return false;
         }
 
         $this->read_prepare($attributes);
 
         return $this->_read($user_dn, $attributes);
     }
 
     public function user_find_by_attribute($attribute)
     {
         return $this->entry_find_by_attribute($attribute);
     }
 
     /**
      * Returns attributes available in specified object classes
      */
     public function attributes_allowed($objectclasses = array())
     {
         $attributes = parent::attributes_allowed($objectclasses);
 
         // additional special attributes that aren't in LDAP schema
         $additional_attributes = array(
             'top' => array('nsRoleDN'),
             '*'   => array('aci'),
         );
 
         if (!empty($attributes)) {
             foreach ($additional_attributes as $class => $attrs) {
                 if (in_array($class, $objectclasses)) {
                     $attributes['may'] = array_merge($attributes['may'], $attrs);
                 }
             }
 
             $attributes['may'] = array_merge($attributes['may'], $additional_attributes['*']);
         }
 
         return $attributes;
     }
 
     /**
      * Find domain by name
      *
      * @param string $domain     Domain name
      * @param array  $attributes Result attributes
      *
      * @return array|bool Domain attributes (+ 'dn' attribute) or False on error
      */
     public function find_domain($domain, $attributes = array('*'))
     {
         if (empty($domain)) {
             return false;
         }
 
         $ckey  = 'domain::' . $domain . '::' . md5(implode(',', $attributes));
 
         if (isset($this->icache[$ckey])) {
             return $this->icache[$ckey];
         }
 
         // connect and bind...
         if ($_SESSION['user'] && $_SESSION['user']->user_bind_dn) {
             $bind_dn = $_SESSION['user']->user_bind_dn;
             $bind_pw = $_SESSION['user']->user_bind_pw;
         }
         else {
             $bind_dn = $this->conf->get('service_bind_dn');
             $bind_pw = $this->conf->get('service_bind_pw');
         }
 
         if (!$this->bind($bind_dn, $bind_pw)) {
             return false;
         }
 
         return parent::find_domain($domain, $attributes);
     }
 
     /**
      * Wrapper for search_entries()
      */
     protected function _list($base_dn, $filter, $scope, $attributes, $search, $params)
     {
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         if (!empty($params['sort_by'])) {
             if (is_array($params['sort_by'])) {
                 foreach ($params['sort_by'] as $attrib) {
                     if (!in_array($attrib, $attributes)) {
                         $attributes[] = $attrib;
                     }
                 }
             } else {
                 if (!in_array($params['sort_by'], $attributes)) {
                     $attributes[] = $params['sort_by'];
                 }
             }
         }
 
         if (!empty($params['page_size'])) {
             $this->config_set('page_size', $params['page_size']);
         } else {
             $this->config_set('page_size', 15);
         }
 
         if (!empty($params['page'])) {
             $this->config_set('list_page', $params['page']);
         } else {
             $this->config_set('list_page', 1);
         }
 
         if (empty($attributes) || !is_array($attributes)) {
             $attributes = array('*');
         }
 
         // LDAP3 search parameters
         $opts = array(
             'search' => $search,
             'sort'   => $params['sort_by'], // for VLV
         );
 
         $result  = $this->search_entries($base_dn, $filter, $scope, $attributes, $opts);
         $entries = $this->sort_and_slice($result, $params);
 
         return array(
             'list'  => $entries,
             'count' => is_object($result) ? $result->count() : 0,
         );
     }
 
     /**
      * Prepare environment before _read() call
      */
     protected function read_prepare(&$attributes)
     {
         // always return unique attribute
         $unique_attr = $this->conf->get('unique_attribute');
         if (empty($unique_attr)) {
             $unique_attr = 'nsuniqueid';
         }
 
         $this->_log(LOG_NOTICE, "Using unique_attribute " . var_export($unique_attr, TRUE) . " at " . __FILE__ . ":" . __LINE__);
 
         if (!in_array($unique_attr, $attributes)) {
             $attributes[] = $unique_attr;
         }
     }
 
     /**
      * delete_entry() wrapper with binding and DN resolving
      */
     protected function entry_delete($entry, $attributes = array(), $base_dn = null)
     {
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         $entry_dn = $this->entry_dn($entry, $attributes, $base_dn);
 
         // object not found or self deletion
         if (!$entry_dn) {
             return false;
         }
 
         return $this->delete_entry($entry_dn);
     }
 
     /**
      * add_entry() wrapper with binding
      */
     protected function entry_add($entry_dn, $attrs)
     {
         $this->bind($_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw);
 
         if ($this->add_entry($entry_dn, $attrs)) {
             return $entry_dn;
         }
 
         return false;
     }
 
     /**
      * Return base DN for specified object type
      */
     protected function entry_base_dn($type, $typeid = null, &$attrs = array())
     {
         // check if base_dn already exists in object attributes
         if (!empty($attrs)) {
             if (!empty($attrs['base_dn'])) {
                 $base_dn = $attrs['base_dn'];
                 unset($attrs['base_dn']);
             }
             else if ($type != 'ou' && !empty($attrs['ou'])) {
                 $base_dn = $attrs['ou'];
                 unset($attrs['ou']);
             }
         }
 
         if (empty($base_dn) && $typeid) {
             $db    = SQL::get_instance();
             $query = $db->query("SELECT `key` FROM `{$type}_types` WHERE `id` = ?", array($typeid));
             $sql   = $db->fetch_assoc($query);
 
             // Check if the type has a specific base DN specified.
             $base_dn = $this->_subject_base_dn($sql['key'] . '_' . $type, true);
         }
 
         if (empty($base_dn)) {
             $base_dn = $this->_subject_base_dn($type);
         }
 
         return $base_dn;
     }
 
     public function _log($level, $msg)
     {
         if (strstr($_SERVER["REQUEST_URI"], "/api/")) {
             $str = "(api) ";
         } else {
             $str = "";
         }
 
         if (is_array($msg)) {
             $msg = implode("\n", $msg);
         }
 
         switch ($level) {
             case LOG_DEBUG:
                 Log::debug($str . $msg);
                 break;
             case LOG_ERR:
             case LOG_ALERT:
             case LOG_CRIT:
             case LOG_EMERG:
                 Log::error($str . $msg);
                 break;
             case LOG_INFO:
                 Log::info($str . $msg);
                 break;
             case LOG_WARNING:
                 Log::warning($str . $msg);
                 break;
             case LOG_NOTICE:
             default:
                 Log::trace($str . $msg);
                 break;
         }
     }
 
     private function _subject_base_dn($subject, $strict = false, $domain = null)
     {
         if (empty($domain)) {
             $domain = $this->domain;
         }
 
         $subject_base_dn = $this->conf->get_raw($domain, $subject . "_base_dn");
 
         if (empty($subject_base_dn)) {
             $subject_base_dn = $this->conf->get_raw("ldap", $subject . "_base_dn");
         }
 
         // This could be "<object_type>_<object_name>", if so we'll try the name only now
         if (empty($subject_base_dn) && ($pos = strrpos($subject, '_'))) {
             $subject         = substr($subject, $pos + 1);
             $subject_base_dn = $this->conf->get_raw($domain, $subject . "_base_dn");
 
             if (empty($subject_base_dn)) {
                 $subject_base_dn = $this->conf->get_raw("ldap", $subject . "_base_dn");
             }
         }
 
         if (empty($subject_base_dn) && $strict) {
             $this->_log(LOG_DEBUG, "subject_base_dn for subject $subject not found");
             return null;
         }
 
         // Attempt to get a configured base_dn
         $base_dn = $this->conf->get($domain, "base_dn");
 
         if (empty($base_dn)) {
             $base_dn = $this->domain_root_dn($domain);
         }
 
         if (!empty($subject_base_dn)) {
             $base_dn = $this->conf->expand($subject_base_dn, array("base_dn" => $base_dn));
         }
 
         $this->_log(LOG_DEBUG, "subject_base_dn for subject $subject is $base_dn");
 
         return $base_dn;
     }
 
     private function legacy_rights($subject)
     {
         $subject_dn    = $this->entry_dn($subject);
         $user_is_admin = false;
         $user_is_self  = false;
 
         // List group memberships
         $user_groups = $this->find_user_groups($_SESSION['user']->user_bind_dn);
 
         foreach ($user_groups as $user_group_dn) {
             if ($user_is_admin)
                 continue;
 
             $user_group_dn_components = ldap_explode_dn($user_group_dn, 1);
             unset($user_group_dn_components["count"]);
             $user_group_cn = array_shift($user_group_dn_components);
             if (in_array($user_group_cn, array('admin', 'maintainer', 'domain-maintainer'))) {
                 // All rights default to write.
                 $user_is_admin = true;
             } else {
                 // The user is a regular user, see if the subject is the same has the
                 // user session's bind_dn.
                 if ($subject_dn == $_SESSION['user']->user_bind_dn) {
                     $user_is_self = true;
                 }
             }
         }
 
         if ($user_is_admin) {
             $standard_rights = array("add", "delete", "read", "write");
         } elseif ($user_is_self) {
             $standard_rights = array("read", "write");
         } else {
             $standard_rights = array("read");
         }
 
         $rights = array(
             'entrylevelrights' => $standard_rights,
             'attributelevelrights' => array(),
         );
 
         $subject = $this->search($subject_dn);
 
         if (!$subject) {
             return $rights;
         }
 
         $subject    = $subject->entries(true);
         $attributes = $this->attributes_allowed($subject[$subject_dn]['objectclass']);
         $attributes = array_merge((array)$attributes['may'], (array)$attributes['must']);
 
         foreach ($attributes as $attribute) {
             $rights['attributelevelrights'][$attribute] = $standard_rights;
         }
 
         return $rights;
     }
 
     private function sort_and_slice(&$result, &$params)
     {
         if (!is_object($result)) {
             return array();
         }
 
         $entries = $result->entries(true);
 
         if ($this->vlv_active) {
             return $entries;
         }
 
         if (!empty($params) && is_array($params)) {
             if (!empty($params['sort_by'])) {
                 $this->sort_result_key = $params['sort_by'];
                 uasort($entries, array($this, 'sort_result'));
             }
 
             if (!empty($params['sort_order']) && $params['sort_order'] == "DESC") {
                 $entries = array_reverse($entries, true);
             }
 
             if (!empty($params['page_size']) && !empty($params['page'])) {
                 if ($result->count() > $params['page_size']) {
                     $entries = array_slice($entries, (($params['page'] - 1) * $params['page_size']), $params['page_size'], true);
                 }
             }
         }
 
         return $entries;
     }
 
     /**
      * Result sorting callback for uasort()
      */
     private function sort_result($a, $b)
     {
         if (is_array($this->sort_result_key)) {
             foreach ($this->sort_result_key as $attrib) {
                 if (array_key_exists($attrib, $a) && !$str1) {
                     $str1 = $a[$attrib];
                 }
                 if (array_key_exists($attrib, $b) && !$str2) {
                     $str2 = $b[$attrib];
                 }
             }
         } else {
             $str1 = $a[$this->sort_result_key];
             $str2 = $b[$this->sort_result_key];
         }
 
         if (is_array($str1)) {
             $str1 = array_shift($str1);
         }
         if (is_array($str2)) {
             $str2 = array_shift($str2);
         }
 
         return strcmp(mb_strtoupper($str1), mb_strtoupper($str2));
     }
 
     /**
      * Qualify a username.
      *
      * Where username is 'kanarip@kanarip.com', the function will return an
      * array containing 'kanarip' and 'kanarip.com'. However, where the
      * username is 'kanarip', the domain name is to be assumed the
      * management domain name.
      */
     private function _qualify_id($username)
     {
         $username_parts = explode('@', $username);
         if (count($username_parts) == 1) {
             $domain_name = $this->conf->get('primary_domain');
         }
         else {
             $domain_name = array_pop($username_parts);
         }
 
         return array(implode('@', $username_parts), $domain_name);
     }
 
     /***********************************************************
      ************      Shortcut functions       ****************
      ***********************************************************/
 
     protected function _read($entry_dn, $attributes = array('*'))
     {
         $result = $this->search($entry_dn, '(objectclass=*)', 'base', $attributes);
 
         if ($result) {
             $this->_log(LOG_DEBUG, "Auth::LDAP::_read() result: " . var_export($result->entries(true), true));
             return $result->entries(true);
         }
         else {
             return false;
         }
     }
 
     /**
      * Finds nsslapd-directory for specified domain
      */
     protected function nsslapd_directory($ldap, $domain)
     {
         $primary_domain  = $this->conf->get('kolab', 'primary_domain');
         $_primary_domain = str_replace('.', '_', $primary_domain);
         $_domain         = str_replace('.', '_', $domain);
         $roots           = array($_primary_domain, $primary_domain, 'userRoot');
 
         foreach ($roots as $root) {
             if ($result = $ldap->get_entry("cn=$root,cn=ldbm database,cn=plugins,cn=config")) {
                 break;
             }
         }
 
         $this->_log(LOG_DEBUG, "Primary domain ldbm database configuration entry: " . var_export($result, true));
 
         $result         = $result[key($result)];
         $orig_directory = $result['nsslapd-directory'];
         $directory      = $orig_directory;
 
         reset($roots);
         foreach ($roots as $root) {
             if ($directory == $orig_directory) {
                 $directory = str_replace($root, $_domain, $result['nsslapd-directory']);
             }
         }
 
         $this->_log(LOG_DEBUG, "nsslapd-directory for domain $domain is $directory");
 
         return $directory;
     }
 }