Merge branch '2013.10' into 2014.11
[tine20] / tine20 / Tinebase / Group / Ldap.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  Group
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  */
11
12 /**
13  * Group ldap backend
14  * 
15  * @package     Tinebase
16  * @subpackage  Group
17  */
18 class Tinebase_Group_Ldap extends Tinebase_Group_Sql implements Tinebase_Group_Interface_SyncAble
19 {
20     const PLUGIN_SAMBA = 'Tinebase_Group_LdapPlugin_Samba';
21     
22     /**
23      * the ldap backend
24      *
25      * @var Tinebase_Ldap
26      */
27     protected $_ldap;
28     
29     /**
30      * ldap config options
31      *
32      * @var array
33      */
34     protected $_options;
35     
36     /**
37      * list of plugins 
38      * 
39      * @var array
40      */
41     protected $_plugins = array();
42     
43     /**
44      * name of the ldap attribute which identifies a group uniquely
45      * for example gidNumber, entryUUID, objectGUID
46      * @var string
47      */
48     protected $_groupUUIDAttribute;
49     
50     /**
51      * name of the ldap attribute which identifies a user uniquely
52      * for example uidNumber, entryUUID, objectGUID
53      * @var string
54      */
55     protected $_userUUIDAttribute;
56     
57     /**
58      * the basic group ldap filter (for example the objectclass)
59      *
60      * @var string
61      */
62     protected $_groupBaseFilter      = 'objectclass=posixgroup';
63     
64     /**
65      * the basic user ldap filter (for example the objectclass)
66      *
67      * @var string
68      */
69     protected $_userBaseFilter      = 'objectclass=posixaccount';
70     
71     /**
72      * the basic user search scope
73      *
74      * @var integer
75      */
76     protected $_groupSearchScope     = Zend_Ldap::SEARCH_SCOPE_SUB;
77     
78     /**
79      * the basic user search scope
80      *
81      * @var integer
82      */
83     protected $_userSearchScope      = Zend_Ldap::SEARCH_SCOPE_SUB;
84     
85     protected $_isReadOnlyBackend    = false;
86     
87     protected $_isDisabledBackend    = false;
88     
89     /**
90      * the constructor
91      *
92      * @param  array $options Options used in connecting, binding, etc.
93      */
94     public function __construct(array $_options) 
95     {
96         parent::__construct();
97         
98         if(empty($_options['userUUIDAttribute'])) {
99             $_options['userUUIDAttribute'] = 'entryUUID';
100         }
101         if(empty($_options['groupUUIDAttribute'])) {
102             $_options['groupUUIDAttribute'] = 'entryUUID';
103         }
104         if(empty($_options['baseDn'])) {
105             $_options['baseDn'] = $_options['userDn'];
106         }
107         if(empty($_options['userFilter'])) {
108             $_options['userFilter'] = 'objectclass=posixaccount';
109         }
110         if(empty($_options['userSearchScope'])) {
111             $_options['userSearchScope'] = Zend_Ldap::SEARCH_SCOPE_SUB;
112         }
113         if(empty($_options['groupFilter'])) {
114             $_options['groupFilter'] = 'objectclass=posixgroup';
115         }
116         
117         $this->_options = $_options;
118         
119         if ((isset($_options['readonly']) || array_key_exists('readonly', $_options))) {
120             $this->_isReadOnlyBackend = (bool)$_options['readonly'];
121         }
122         if ((isset($_options['ldap']) || array_key_exists('ldap', $_options))) {
123             $this->_ldap = $_options['ldap'];
124         }
125         if (isset($this->_options['requiredObjectClass'])) {
126             $this->_requiredObjectClass = (array)$this->_options['requiredObjectClass'];
127         }
128         if (! array_key_exists('groupsDn', $this->_options) || empty($this->_options['groupsDn'])) {
129             $this->_isDisabledBackend = true;
130         }
131         
132         $this->_userUUIDAttribute  = strtolower($this->_options['userUUIDAttribute']);
133         $this->_groupUUIDAttribute = strtolower($this->_options['groupUUIDAttribute']);
134         $this->_baseDn             = $this->_options['baseDn'];
135         $this->_userBaseFilter     = $this->_options['userFilter'];
136         $this->_userSearchScope    = $this->_options['userSearchScope'];
137         $this->_groupBaseFilter    = $this->_options['groupFilter'];
138         
139         if (isset($_options['plugins']) && is_array($_options['plugins'])) {
140             foreach ($_options['plugins'] as $className) {
141                 $this->_plugins[$className] = new $className($this->getLdap(), $this->_options);
142             }
143         }
144     }
145     
146     /**
147      * get syncable group by id from sync backend
148      * 
149      * @param  mixed  $_groupId  the groupid
150      * 
151      * @return Tinebase_Model_Group
152      */
153     public function getGroupByIdFromSyncBackend($_groupId)
154     {
155         if ($this->isDisabledBackend()) {
156             throw new Tinebase_Exception_UnexpectedValue('backend is disabled');
157         }
158         
159         $groupId = Tinebase_Model_Group::convertGroupIdToInt($_groupId);
160         
161         $filter = Zend_Ldap_Filter::andFilter(
162             Zend_Ldap_Filter::string($this->_groupBaseFilter),
163             Zend_Ldap_Filter::equals($this->_groupUUIDAttribute, $this->_encodeGroupId($groupId))
164         );
165         
166         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
167             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " ldap filter: " . $filter);
168         
169         $groups = $this->getLdap()->search(
170             $filter, 
171             $this->_options['groupsDn'], 
172             $this->_groupSearchScope, 
173             array('cn', 'description', $this->_groupUUIDAttribute)
174         );
175         
176         if (count($groups) == 0) {
177             throw new Tinebase_Exception_Record_NotDefined('Group not found.');
178         }
179
180         $group = $groups->getFirst();
181         
182         $result = new Tinebase_Model_Group(array(
183             'id'            => $this->_decodeGroupId($group[$this->_groupUUIDAttribute][0]),
184             'name'          => $group['cn'][0],
185             'description'   => isset($group['description'][0]) ? $group['description'][0] : '' 
186         ), TRUE);
187         
188         return $result;
189     }
190     
191     /**
192      * get list of groups from syncbackend
193      *
194      * @todo make filtering working. Allways returns all groups
195      *
196      * @param  string  $_filter
197      * @param  string  $_sort
198      * @param  string  $_dir
199      * @param  int     $_start
200      * @param  int     $_limit
201      * 
202      * @return Tinebase_Record_RecordSet with record class Tinebase_Model_Group
203      */
204     public function getGroupsFromSyncBackend($_filter = NULL, $_sort = 'name', $_dir = 'ASC', $_start = NULL, $_limit = NULL)
205     {
206         if ($this->isDisabledBackend()) {
207             throw new Tinebase_Exception_UnexpectedValue('backend is disabled');
208         }
209         
210         $filter = Zend_Ldap_Filter::string($this->_groupBaseFilter);
211         
212         $groups = $this->getLdap()->search(
213             $filter, 
214             $this->_options['groupsDn'], 
215             $this->_groupSearchScope, 
216             array('cn', 'description', $this->_groupUUIDAttribute)
217         );
218         
219         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Group');
220         
221         foreach ($groups as $group) {
222             $groupObject = new Tinebase_Model_Group(array(
223                 'id'            => $this->_decodeGroupId($group[$this->_groupUUIDAttribute][0]),
224                 'name'          => $group['cn'][0],
225                 'description'   => isset($group['description'][0]) ? $group['description'][0] : null
226             ), TRUE);
227
228             $result->addRecord($groupObject);
229         }
230         
231         return $result;
232     }
233     
234     /**
235      * get ldap connection handling class
236      * 
237      * @throws Tinebase_Exception_Backend_Ldap
238      * @return Tinebase_Ldap
239      */
240     public function getLdap()
241     {
242         if (! $this->_ldap instanceof Tinebase_Ldap) {
243             $this->_ldap = new Tinebase_Ldap($this->_options);
244             try {
245                 $this->getLdap()->bind();
246             } catch (Zend_Ldap_Exception $zle) {
247                 // @todo move this to Tinebase_Ldap?
248                 throw new Tinebase_Exception_Backend_Ldap('Could not bind to LDAP: ' . $zle->getMessage());
249             }
250         }
251         
252         return $this->_ldap;
253     }
254     
255     /**
256      * (non-PHPdoc)
257      * @see Tinebase_Group_Interface_SyncAble::isReadOnlyBackend()
258      */
259     public function isReadOnlyBackend()
260     {
261         return $this->_isReadOnlyBackend;
262     }
263     
264     /**
265      * (non-PHPdoc)
266      * @see Tinebase_Group_Interface_SyncAble::isDisabledBackend()
267      */
268     public function isDisabledBackend()
269     {
270         return $this->_isDisabledBackend;
271     }
272     
273     /**
274      * replace all current groupmembers with the new groupmembers list in sync backend
275      *
276      * @param  string  $_groupId
277      * @param  array   $_groupMembers array of ids
278      * @return array with current group memberships (account ids)
279      */
280     public function setGroupMembersInSyncBackend($_groupId, $_groupMembers) 
281     {
282         if ($this->isDisabledBackend() || $this->isReadOnlyBackend()) {
283             return $_groupMembers;
284         }
285         
286         $metaData = $this->_getMetaData($_groupId);
287         
288         $membersMetaDatas = $this->_getAccountsMetaData((array)$_groupMembers, FALSE);
289         if (count($_groupMembers) !== count($membersMetaDatas)) {
290             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
291                 . ' Removing ' . (count($_groupMembers) - count($membersMetaDatas)) . ' no longer existing group members from group ' . $_groupId);
292             
293             $_groupMembers = array();
294             foreach ($membersMetaDatas as $account) {
295                 $_groupMembers[] = $account[$this->_userUUIDAttribute];
296             }
297         }
298         
299         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
300             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $group data: ' . print_r($metaData, true));
301         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
302             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $memebers: ' . print_r($membersMetaDatas, true));
303         
304         $groupDn = $this->_getDn($_groupId);
305         
306         $memberDn = array();
307         $memberUid = array();
308         
309         foreach ($membersMetaDatas as $memberMetadata) {
310             $memberDn[]  = $memberMetadata['dn'];
311             $memberUid[] = $memberMetadata['uid'];
312         }
313         
314         $ldapData = array(
315             'memberuid' => $memberUid
316         );
317         
318         if ($this->_options['useRfc2307bis']) {
319             if (!empty($memberDn)) {
320                 $ldapData['member'] = $memberDn; // array of dns
321             } else {
322                 $ldapData['member'] = $groupDn; // single dn
323             }
324         }
325         
326         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
327             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $dn: ' . $metaData['dn']);
328         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
329             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $ldapData: ' . print_r($ldapData, true));
330         
331         $this->getLdap()->update($metaData['dn'], $ldapData);
332         
333         return $_groupMembers;
334     }
335     
336     /**
337      * replace all current groupmemberships of user in sync backend
338      *
339      * @param  mixed  $_userId
340      * @param  mixed  $_groupIds
341      * 
342      * @return array
343      */
344     public function setGroupMembershipsInSyncBackend($_userId, $_groupIds)
345     {
346         if ($this->isDisabledBackend() || $this->isReadOnlyBackend()) {
347             return $_groupIds;
348         }
349         
350         if ($_groupIds instanceof Tinebase_Record_RecordSet) {
351             $_groupIds = $_groupIds->getArrayOfIds();
352         }
353         
354         if(count($_groupIds) === 0) {
355             throw new Tinebase_Exception_InvalidArgument('user must belong to at least one group');
356         }
357         
358         $userId = Tinebase_Model_user::convertUserIdToInt($_userId);
359         
360         $groupMemberships = $this->getGroupMembershipsFromSyncBackend($userId);
361         
362         $removeGroupMemberships = array_diff($groupMemberships, $_groupIds);
363         $addGroupMemberships    = array_diff($_groupIds, $groupMemberships);
364         
365         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' current groupmemberships: ' . print_r($groupMemberships, true));
366         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' new groupmemberships: ' . print_r($_groupIds, true));
367         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' added groupmemberships: ' . print_r($addGroupMemberships, true));
368         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' removed groupmemberships: ' . print_r($removeGroupMemberships, true));
369         
370         foreach ($addGroupMemberships as $groupId) {
371             $this->addGroupMemberInSyncBackend($groupId, $userId);
372         }
373         
374         foreach ($removeGroupMemberships as $groupId) {
375             $this->removeGroupMemberInSyncBackend($groupId, $userId);
376         }
377         
378         return $this->getGroupMembershipsFromSyncBackend($userId);
379     }
380     
381     /**
382      * add a new groupmember to group in sync backend
383      *
384      * @param  mixed  $_groupId
385      * @param  mixed  $_accountId string or user object
386      */
387     public function addGroupMemberInSyncBackend($_groupId, $_accountId) 
388     {
389         if ($this->isDisabledBackend() || $this->isReadOnlyBackend()) {
390             return;
391         }
392         
393         $userId  = Tinebase_Model_User::convertUserIdToInt($_accountId);
394         $groupId = Tinebase_Model_Group::convertGroupIdToInt($_groupId);
395         
396         $memberships = $this->getGroupMembershipsFromSyncBackend($_accountId);
397         if (in_array($groupId, $memberships)) {
398              if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " skip adding group member, as $userId is already in group $groupId");
399              return;
400         }
401         
402         $groupDn = $this->_getDn($_groupId);
403         $ldapData = array();
404         
405         $accountMetaData = $this->_getAccountMetaData($_accountId);
406         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " account meta data: " . print_r($accountMetaData, true));
407         
408         $filter = Zend_Ldap_Filter::andFilter(
409             Zend_Ldap_Filter::equals($this->_groupUUIDAttribute, $this->_encodeGroupId($groupId)),
410             Zend_Ldap_Filter::equals('memberuid', Zend_Ldap::filterEscape($accountMetaData['uid']))
411         );
412         
413         $groups = $this->getLdap()->search(
414             $filter, 
415             $this->_options['groupsDn'], 
416             $this->_groupSearchScope, 
417             array('dn')
418         );
419
420         if (count($groups) == 0) {
421             // need to add memberuid
422             $ldapData['memberuid'] = $accountMetaData['uid'];
423         }
424         
425         
426         if ($this->_options['useRfc2307bis']) {
427             $filter = Zend_Ldap_Filter::andFilter(
428                 Zend_Ldap_Filter::equals($this->_groupUUIDAttribute, $this->_encodeGroupId($groupId)),
429                 Zend_Ldap_Filter::equals('member', Zend_Ldap::filterEscape($accountMetaData['dn']))
430             );
431             
432             $groups = $this->getLdap()->search(
433                 $filter, 
434                 $this->_options['groupsDn'], 
435                 $this->_groupSearchScope, 
436                 array('dn')
437             );
438             
439             if (count($groups) == 0) {
440                 // need to add member
441                 $ldapData['member'] = $accountMetaData['dn'];
442             }
443         }
444                 
445         if (!empty($ldapData)) {
446             $this->getLdap()->addProperty($groupDn, $ldapData);
447         }
448         
449         if ($this->_options['useRfc2307bis']) {
450             // remove groupdn if no longer needed
451             $filter = Zend_Ldap_Filter::andFilter(
452                 Zend_Ldap_Filter::equals($this->_groupUUIDAttribute, $this->_encodeGroupId($groupId)),
453                 Zend_Ldap_Filter::equals('member', Zend_Ldap::filterEscape($groupDn))
454             );
455             
456             $groups = $this->getLdap()->search(
457                 $filter, 
458                 $this->_options['groupsDn'], 
459                 $this->_groupSearchScope, 
460                 array('dn')
461             );
462             
463             if (count($groups) > 0) {
464                 $ldapData = array (
465                     'member' => $groupDn
466                 );
467                 $this->getLdap()->deleteProperty($groupDn, $ldapData);
468             }
469         }
470     }
471
472     /**
473      * remove one member from the group in sync backend
474      *
475      * @param  mixed  $_groupId
476      * @param  mixed  $_accountId
477      */
478     public function removeGroupMemberInSyncBackend($_groupId, $_accountId) 
479     {
480         if ($this->isDisabledBackend() || $this->isReadOnlyBackend()) {
481             return;
482         }
483         
484         $userId  = Tinebase_Model_User::convertUserIdToInt($_accountId);
485         $groupId = Tinebase_Model_Group::convertGroupIdToInt($_groupId);
486         
487         $memberships = $this->getGroupMemberships($_accountId);
488         if (!in_array($groupId, $memberships)) {
489              if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " skip removing group member, as $userId is not in group $groupId " . print_r($memberships, true));
490              return;
491         }
492         
493         try {
494             $groupDn = $this->_getDn($_groupId);
495         } catch (Tinebase_Exception_NotFound $tenf) {
496             if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . 
497                 " Failed to remove groupmember $_accountId from group $_groupId: " . $tenf->getMessage()
498             );
499             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $tenf->getTraceAsString());
500             return;
501         }
502         
503         try {
504             $accountMetaData = $this->_getAccountMetaData($_accountId);
505         } catch (Tinebase_Exception_NotFound $tenf) {
506             if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . ' user not found in sync backend: ' . $_accountId);
507             return;
508         }
509         
510         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " account meta data: " . print_r($accountMetaData, true));
511         
512         $memberUidNumbers = $this->getGroupMembers($_groupId);
513         
514         $ldapData = array(
515             'memberuid' => $accountMetaData['uid']
516         );
517         
518         if (isset($this->_options['useRfc2307bis']) && $this->_options['useRfc2307bis']) {
519             
520             if (count($memberUidNumbers) === 1) {
521                 // we need to add the group dn, as the member attribute is not allowed to be empty
522                 $dataAdd = array(
523                     'member' => $groupDn
524                 );
525                 $this->getLdap()->addProperty($groupDn, $dataAdd);
526             } else {
527                 $ldapData['member'] = $accountMetaData['dn'];
528             }
529         }
530             
531         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $dn: ' . $groupDn);
532         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $ldapData: ' . print_r($ldapData, true));
533         
534         try {
535             $this->getLdap()->deleteProperty($groupDn, $ldapData);
536         } catch (Zend_Ldap_Exception $zle) {
537             if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . 
538                 " Failed to remove groupmember {$accountMetaData['dn']} from group $groupDn: " . $zle->getMessage()
539             );
540             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $zle->getTraceAsString());
541         }
542     }
543     
544     /**
545      * create a new group in sync backend
546      *
547      * @param  Tinebase_Model_Group  $_group
548      * 
549      * @return Tinebase_Model_Group|NULL
550      */
551     public function addGroupInSyncBackend(Tinebase_Model_Group $_group) 
552     {
553         if ($this->isDisabledBackend() || $this->isReadOnlyBackend()) {
554             return $_group;
555         }
556         
557         $dn = $this->_generateDn($_group);
558         $objectClass = array(
559             'top',
560             'posixGroup'
561         );
562         
563         $gidNumber = $this->_generateGidNumber();
564         $ldapData = array(
565             'objectclass' => $objectClass,
566             'gidnumber'   => $gidNumber,
567             'cn'          => $_group->name,
568             'description' => $_group->description,
569         );
570         
571         if (isset($this->_options['useRfc2307bis']) && $this->_options['useRfc2307bis'] == true) {
572             $ldapData['objectclass'][] = 'groupOfNames';
573             // the member attribute can not be emtpy, seems to be common praxis 
574             // to set the member attribute to the group dn itself for empty groups
575             $ldapData['member']        = $dn;
576         }
577         
578         foreach ($this->_plugins as $plugin) {
579             $plugin->inspectAddGroup($_group, $ldapData);
580         }
581         
582         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $dn: ' . $dn);
583         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $ldapData: ' . print_r($ldapData, true));
584         $this->getLdap()->add($dn, $ldapData);
585         
586         $groupId = $this->getLdap()->getEntry($dn, array($this->_groupUUIDAttribute));
587         
588         $groupId = $this->_decodeGroupId($groupId[$this->_groupUUIDAttribute][0]);
589         
590         $group = $this->getGroupByIdFromSyncBackend($groupId);
591                 
592         return $group;
593     }
594     
595     /**
596      * updates an existing group in sync backend
597      *
598      * @param  Tinebase_Model_Group  $_group
599      * 
600      * @return Tinebase_Model_Group
601      */
602     public function updateGroupInSyncBackend(Tinebase_Model_Group $_group) 
603     {
604         if ($this->isDisabledBackend() || $this->isReadOnlyBackend()) {
605             return $_group;
606         }
607         
608         $metaData = $this->_getMetaData($_group->getId());
609         $dn = $metaData['dn'];
610         
611         $ldapData = array(
612             'cn'          => $_group->name,
613             'description' => $_group->description,
614             'objectclass' => $metaData['objectclass']
615         );
616         
617         foreach ($this->_plugins as $plugin) {
618             $plugin->inspectUpdateGroup($_group, $ldapData);
619         }
620         
621         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
622             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $dn: ' . $dn);
623         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
624             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $ldapData: ' . print_r($ldapData, true));
625         
626         $this->getLdap()->update($dn, $ldapData);
627         
628         if ($metaData['cn'] != $ldapData['cn']) {
629             $newDn = "cn={$ldapData['cn']},{$this->_options['groupsDn']}";
630             $this->_ldap->rename($dn, $newDn);
631         }
632         
633         $group = $this->getGroupByIdFromSyncBackend($_group);
634
635         return $group;
636     }
637     
638     /**
639      * delete one or more groups in sync backend
640      *
641      * @param  mixed   $_groupId
642      */
643     public function deleteGroupsInSyncBackend($_groupId) 
644     {
645         if ($this->isDisabledBackend() || $this->isReadOnlyBackend()) {
646             return;
647         }
648         
649         $groupIds = array();
650         
651         if (is_array($_groupId) or $_groupId instanceof Tinebase_Record_RecordSet) {
652             foreach ($_groupId as $groupId) {
653                 $groupIds[] = Tinebase_Model_Group::convertGroupIdToInt($groupId);
654             }
655         } else {
656             $groupIds[] = Tinebase_Model_Group::convertGroupIdToInt($_groupId);
657         }
658         
659         foreach ($groupIds as $groupId) {
660             try {
661                 $dn = $this->_getDn($groupId);
662             } catch (Tinebase_Exception_NotFound $tenf) {
663                 // group does not exist in LDAP backend any more
664                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
665                     . ' Did not found group with id ' . $groupId . ' in LDAP. Delete skipped!');
666                 continue;
667             }
668             
669             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
670                 . ' Deleting group ' . $dn . ' from LDAP');
671             $this->getLdap()->delete($dn);
672         }
673     }
674     
675     /**
676      * get dn of an existing group
677      *
678      * @param  string $_groupId
679      * @return string 
680      */
681     protected function _getDn($_groupId)
682     {
683         $metaData = $this->_getMetaData($_groupId);
684         
685         return $metaData['dn'];
686     }
687     
688     /**
689      * returns ldap metadata of given group
690      *
691      * @param  string $_groupId
692      * @return array
693      * @throws Tinebase_Exception_NotFound
694      * 
695      * @todo remove obsolete code
696      */
697     protected function _getMetaData($_groupId)
698     {
699         $groupId = Tinebase_Model_Group::convertGroupIdToInt($_groupId);
700         
701         $filter = Zend_Ldap_Filter::equals(
702             $this->_groupUUIDAttribute, $this->_encodeGroupId($groupId)
703         );
704         
705         $result = $this->getLdap()->search(
706             $filter, 
707             $this->_options['groupsDn'], 
708             $this->_groupSearchScope, 
709             array('objectclass')
710         );
711         
712         if (count($result) !== 1) {
713             throw new Tinebase_Exception_NotFound("Group with id $_groupId not found.");
714         }
715         
716         return $result->getFirst();
717     }
718     
719     /**
720      * get metatada of existing user
721      *
722      * @param  string  $_userId
723      * @return array
724      */
725     protected function _getUserMetaData($_userId)
726     {
727         $userId = $this->_encodeAccountId(Tinebase_Model_User::convertUserIdToInt($_userId));
728
729         $filter = Zend_Ldap_Filter::equals(
730             $this->_userUUIDAttribute, $userId
731         );
732
733         $result = $this->getLdap()->search(
734             $filter,
735             $this->_baseDn,
736             $this->_userSearchScope
737         );
738
739         if (count($result) !== 1) {
740             throw new Tinebase_Exception_NotFound("user with userid $_userId not found");
741         }
742
743         return $result->getFirst();
744     }
745     
746     /**
747      * returns arrays of metainfo from given accountIds
748      *
749      * @param array $_accountIds
750      * @param boolean $throwExceptionOnMissingAccounts
751      * @return array of strings
752      */
753     protected function _getAccountsMetaData(array $_accountIds, $throwExceptionOnMissingAccounts = TRUE)
754     {
755         $filterArray = array();
756         foreach ($_accountIds as $accountId) {
757             $accountId = Tinebase_Model_User::convertUserIdToInt($accountId);
758             $filterArray[] = Zend_Ldap_Filter::equals($this->_userUUIDAttribute, Zend_Ldap::filterEscape($accountId));
759         }
760         $filter = new Zend_Ldap_Filter_Or($filterArray);
761         
762         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $filter: ' . $filter . ' count: ' . count($filterArray));
763         
764         // fetch all dns at once
765         $accounts = $this->getLdap()->search(
766             $filter, 
767             $this->_options['userDn'], 
768             $this->_userSearchScope, 
769             array('uid', $this->_userUUIDAttribute, 'objectclass')
770         );
771         
772         if (count($_accountIds) != count($accounts)) {
773             $wantedAccountIds    = array();
774             $retrievedAccountIds = array();
775             
776             foreach ($_accountIds as $accountId) {
777                 $wantedAccountIds[] = Tinebase_Model_User::convertUserIdToInt($accountId);
778             }
779             foreach ($accounts as $account) {
780                 $retrievedAccountIds[] = $account[$this->_userUUIDAttribute][0];
781             }
782             
783             $message = "Some dn's are missing. "  . print_r(array_diff($wantedAccountIds, $retrievedAccountIds), true);
784             if ($throwExceptionOnMissingAccounts) {
785                 throw new Tinebase_Exception_NotFound($message);
786             } else {
787                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . $message);
788             }
789         }
790         
791         $result = array();
792         foreach ($accounts as $account) {
793             $result[] = array(
794                 'dn'                        => $account['dn'],
795                 'objectclass'               => $account['objectclass'],
796                 'uid'                       => $account['uid'][0],
797                 $this->_userUUIDAttribute   => $account[$this->_userUUIDAttribute][0]
798             );
799         }
800
801         return $result;
802     }
803     
804     /**
805      * convert binary id to plain text id
806      * 
807      * @param  string  $groupId
808      * @return string
809      */
810     protected function _decodeGroupId($groupId)
811     {
812         return $groupId;
813     }
814     
815     /**
816      * helper function to be overwriten in subclasses
817      * 
818      * @param  string  $accountId
819      * @return string
820      */
821     protected function _encodeAccountId($accountId)
822     {
823         return $accountId;
824     }
825     
826     /**
827      * convert binary id to plain text id
828      * 
829      * @param  string  $groupId
830      * @return string
831      */
832     protected function _encodeGroupId($groupId)
833     {
834         return $groupId;
835     }
836     
837     /**
838      * returns a single account dn
839      *
840      * @param string $_accountId
841      * @return string
842      */
843     protected function _getAccountMetaData($_accountId)
844     {
845         return Tinebase_Helper::array_value(0, $this->_getAccountsMetaData(array($_accountId)));
846     }
847     
848     /**
849      * generates a new dn for a group
850      *
851      * @param  Tinebase_Model_Group $_group
852      * @return string
853      */
854     protected function _generateDn(Tinebase_Model_Group $_group)
855     {
856         $newDn = "cn={$_group->name},{$this->_options['groupsDn']}";
857         
858         return $newDn;
859     }
860     
861     /**
862      * generates a gidnumber
863      *
864      * @todo add a persistent registry which id has been generated lastly to
865      *       reduce amount of groupid to be transfered
866      * 
867      * @return int
868      */
869     protected function _generateGidNumber()
870     {
871         $allGidNumbers = array();
872         $gidNumber = null;
873         
874         $filter = Zend_Ldap_Filter::orFilter(
875             Zend_Ldap_Filter::equals('objectclass', 'posixgroup'),
876             Zend_Ldap_Filter::equals('objectclass', 'group')
877         );
878         
879         $groups = $this->getLdap()->search(
880             $filter, 
881             $this->_options['groupsDn'], 
882             Zend_Ldap::SEARCH_SCOPE_SUB, 
883             array('gidnumber')
884         );
885         
886         foreach ($groups as $groupData) {
887             $allGidNumbers[] = $groupData['gidnumber'][0];
888         }
889         sort($allGidNumbers);
890         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . "  Existing gidnumbers " . print_r($allGidNumbers, true));
891         
892         $numGroups = count($allGidNumbers);
893         if ($numGroups == 0 || $allGidNumbers[$numGroups-1] < $this->_options['minGroupId']) {
894             $gidNumber =  $this->_options['minGroupId'];
895         } elseif ($allGidNumbers[$numGroups-1] < $this->_options['maxGroupId']) {
896             $gidNumber = ++$allGidNumbers[$numGroups-1];
897         } elseif (count($allGidNumbers) < ($this->_options['maxGroupId'] - $this->_options['minGroupId'])) {
898             // maybe there is a gap
899             for($i = $this->_options['minGroupId']; $i <= $this->_options['maxGroupId']; $i++) {
900                 if (!in_array($i, $allGidNumbers)) {
901                     $gidNumber = $i;
902                     break;
903                 }
904             }
905         }
906         
907         if ($gidNumber === NULL) {
908             throw new Tinebase_Exception_NotImplemented('Max Group Id is reached');
909         }
910         
911         return $gidNumber;
912     }
913     
914     /**
915      * resolve groupid(for example ldap gidnumber) to uuid(for example ldap entryuuid)
916      *
917      * @param   string  $_groupId
918      * @return  string  the uuid for groupid
919      */
920     public function resolveSyncAbleGidToUUid($_groupId)
921     {
922         if ($this->isDisabledBackend()) {
923             throw new Tinebase_Exception_UnexpectedValue('backend is disabled');
924         }
925         
926         return $this->resolveGIdNumberToUUId($_groupId);
927     }
928     
929     /**
930      * resolve gidnumber to UUID(for example entryUUID) attribute
931      * 
932      * @param int $_gidNumber the gidnumber
933      * @return string 
934      */
935     public function resolveGIdNumberToUUId($_gidNumber)
936     {
937         if ($this->_groupUUIDAttribute == 'gidnumber') {
938             return $_gidNumber;
939         }
940         
941         if ($this->isDisabledBackend()) {
942             throw new Tinebase_Exception_UnexpectedValue('backend is disabled');
943         }
944         
945         $filter = Zend_Ldap_Filter::andFilter(
946             Zend_Ldap_Filter::string($this->_groupBaseFilter),
947             Zend_Ldap_Filter::equals('gidnumber', $_gidNumber)
948         );
949         
950         $groupId = $this->getLdap()->search(
951             $filter, 
952             $this->_options['groupsDn'], 
953             $this->_groupSearchScope, 
954             array($this->_groupUUIDAttribute)
955         )->getFirst();
956         
957         if ($groupId == null) {
958             throw new Tinebase_Exception_NotFound('LDAP group with (gidnumber=' . $_gidNumber . ') not found');
959         }
960         
961         return $groupId[$this->_groupUUIDAttribute][0];
962     }
963     
964     /**
965      * resolve UUID(for example entryUUID) to gidnumber
966      * 
967      * @param string $_uuid
968      * @return string
969      */
970     public function resolveUUIdToGIdNumber($_uuid)
971     {
972         if ($this->_groupUUIDAttribute == 'gidnumber') {
973             return $_uuid;
974         }
975         
976         if ($this->isDisabledBackend()) {
977             throw new Tinebase_Exception_UnexpectedValue('backend is disabled');
978         }
979         
980         $filter = Zend_Ldap_Filter::andFilter(
981             Zend_Ldap_Filter::string($this->_groupBaseFilter),
982             Zend_Ldap_Filter::equals($this->_groupUUIDAttribute, $this->_encodeGroupId($_uuid))
983         );
984         
985         $groupId = $this->getLdap()->search(
986             $filter, 
987             $this->_options['groupsDn'], 
988             $this->_groupSearchScope, 
989             array('gidnumber')
990         )->getFirst();
991         
992         return $groupId['gidnumber'][0];
993     }
994     
995     /**
996      * get groupmemberships of user from sync backend
997      * 
998      * @param   Tinebase_Model_User|string  $_userId
999      * @return  array  list of group ids
1000      */
1001     public function getGroupMembershipsFromSyncBackend($_userId)
1002     {
1003         if (!$this->isDisabledBackend()) {
1004             $metaData = $this->_getUserMetaData($_userId);
1005             
1006             $filter = Zend_Ldap_Filter::andFilter(
1007                 Zend_Ldap_Filter::string($this->_groupBaseFilter),
1008                 Zend_Ldap_Filter::orFilter(
1009                     Zend_Ldap_Filter::equals('memberuid', Zend_Ldap::filterEscape($metaData['uid'][0])),
1010                     Zend_Ldap_Filter::equals('member',    Zend_Ldap::filterEscape($metaData['dn']))
1011                 )
1012             );
1013             
1014             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
1015                 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .' ldap search filter: ' . $filter);
1016             
1017             $groups = $this->getLdap()->search(
1018                 $filter, 
1019                 $this->_options['groupsDn'], 
1020                 $this->_groupSearchScope, 
1021                 array('cn', 'description', $this->_groupUUIDAttribute)
1022             );
1023             
1024             $memberships = array();
1025             
1026             foreach ($groups as $group) {
1027                 $memberships[] = $group[$this->_groupUUIDAttribute][0];
1028             }
1029         } else {
1030             $memberships = $this->getGroupMemberships($_userId);
1031             
1032             if (empty($memberships)) {
1033                 $memberships[] = Tinebase_Group::getInstance()->getDefaultGroup()->getId();
1034             }
1035         }
1036         
1037         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
1038             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .' group memberships: ' . print_r($memberships, TRUE));
1039         
1040         return $memberships;
1041     }
1042     
1043     /**
1044      * (non-PHPdoc)
1045      * @see tine20/Tinebase/Group/Interface/Syncable::mergeMissingProperties
1046      */
1047     public static function mergeMissingProperties($syncGroup, $sqlGroup)
1048     {
1049         // @TODO see ldap schema, email might be an attribute
1050         foreach (array('list_id', 'email', 'visibility') as $property) {
1051             $syncGroup->{$property} = $sqlGroup->{$property};
1052         }
1053     }
1054 }