5e5519bfca7eb605f9e62e1d93c3525a893b5166
[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         $group = $this->getGroupByIdFromSyncBackend($_group);
629
630         return $group;
631     }
632     
633     /**
634      * delete one or more groups in sync backend
635      *
636      * @param  mixed   $_groupId
637      */
638     public function deleteGroupsInSyncBackend($_groupId) 
639     {
640         if ($this->isDisabledBackend() || $this->isReadOnlyBackend()) {
641             return;
642         }
643         
644         $groupIds = array();
645         
646         if (is_array($_groupId) or $_groupId instanceof Tinebase_Record_RecordSet) {
647             foreach ($_groupId as $groupId) {
648                 $groupIds[] = Tinebase_Model_Group::convertGroupIdToInt($groupId);
649             }
650         } else {
651             $groupIds[] = Tinebase_Model_Group::convertGroupIdToInt($_groupId);
652         }
653         
654         foreach ($groupIds as $groupId) {
655             try {
656                 $dn = $this->_getDn($groupId);
657             } catch (Tinebase_Exception_NotFound $tenf) {
658                 // group does not exist in LDAP backend any more
659                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
660                     . ' Did not found group with id ' . $groupId . ' in LDAP. Delete skipped!');
661                 continue;
662             }
663             
664             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
665                 . ' Deleting group ' . $dn . ' from LDAP');
666             $this->getLdap()->delete($dn);
667         }
668     }
669     
670     /**
671      * get dn of an existing group
672      *
673      * @param  string $_groupId
674      * @return string 
675      */
676     protected function _getDn($_groupId)
677     {
678         $metaData = $this->_getMetaData($_groupId);
679         
680         return $metaData['dn'];
681     }
682     
683     /**
684      * returns ldap metadata of given group
685      *
686      * @param  string $_groupId
687      * @return array
688      * @throws Tinebase_Exception_NotFound
689      * 
690      * @todo remove obsolete code
691      */
692     protected function _getMetaData($_groupId)
693     {
694         $groupId = Tinebase_Model_Group::convertGroupIdToInt($_groupId);
695         
696         $filter = Zend_Ldap_Filter::equals(
697             $this->_groupUUIDAttribute, $this->_encodeGroupId($groupId)
698         );
699         
700         $result = $this->getLdap()->search(
701             $filter, 
702             $this->_options['groupsDn'], 
703             $this->_groupSearchScope, 
704             array('objectclass')
705         );
706         
707         if (count($result) !== 1) {
708             throw new Tinebase_Exception_NotFound("Group with id $_groupId not found.");
709         }
710         
711         return $result->getFirst();
712     }
713     
714     /**
715      * get metatada of existing user
716      *
717      * @param  string  $_userId
718      * @return array
719      */
720     protected function _getUserMetaData($_userId)
721     {
722         $userId = $this->_encodeAccountId(Tinebase_Model_User::convertUserIdToInt($_userId));
723
724         $filter = Zend_Ldap_Filter::equals(
725             $this->_userUUIDAttribute, $userId
726         );
727
728         $result = $this->getLdap()->search(
729             $filter,
730             $this->_baseDn,
731             $this->_userSearchScope
732         );
733
734         if (count($result) !== 1) {
735             throw new Tinebase_Exception_NotFound("user with userid $_userId not found");
736         }
737
738         return $result->getFirst();
739     }
740     
741     /**
742      * returns arrays of metainfo from given accountIds
743      *
744      * @param array $_accountIds
745      * @param boolean $throwExceptionOnMissingAccounts
746      * @return array of strings
747      */
748     protected function _getAccountsMetaData(array $_accountIds, $throwExceptionOnMissingAccounts = TRUE)
749     {
750         $filterArray = array();
751         foreach ($_accountIds as $accountId) {
752             $accountId = Tinebase_Model_User::convertUserIdToInt($accountId);
753             $filterArray[] = Zend_Ldap_Filter::equals($this->_userUUIDAttribute, Zend_Ldap::filterEscape($accountId));
754         }
755         $filter = new Zend_Ldap_Filter_Or($filterArray);
756         
757         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $filter: ' . $filter . ' count: ' . count($filterArray));
758         
759         // fetch all dns at once
760         $accounts = $this->getLdap()->search(
761             $filter, 
762             $this->_options['userDn'], 
763             $this->_userSearchScope, 
764             array('uid', $this->_userUUIDAttribute, 'objectclass')
765         );
766         
767         if (count($_accountIds) != count($accounts)) {
768             $wantedAccountIds    = array();
769             $retrievedAccountIds = array();
770             
771             foreach ($_accountIds as $accountId) {
772                 $wantedAccountIds[] = Tinebase_Model_User::convertUserIdToInt($accountId);
773             }
774             foreach ($accounts as $account) {
775                 $retrievedAccountIds[] = $account[$this->_userUUIDAttribute][0];
776             }
777             
778             $message = "Some dn's are missing. "  . print_r(array_diff($wantedAccountIds, $retrievedAccountIds), true);
779             if ($throwExceptionOnMissingAccounts) {
780                 throw new Tinebase_Exception_NotFound($message);
781             } else {
782                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . $message);
783             }
784         }
785         
786         $result = array();
787         foreach ($accounts as $account) {
788             $result[] = array(
789                 'dn'                        => $account['dn'],
790                 'objectclass'               => $account['objectclass'],
791                 'uid'                       => $account['uid'][0],
792                 $this->_userUUIDAttribute   => $account[$this->_userUUIDAttribute][0]
793             );
794         }
795
796         return $result;
797     }
798     
799     /**
800      * convert binary id to plain text id
801      * 
802      * @param  string  $groupId
803      * @return string
804      */
805     protected function _decodeGroupId($groupId)
806     {
807         return $groupId;
808     }
809     
810     /**
811      * helper function to be overwriten in subclasses
812      * 
813      * @param  string  $accountId
814      * @return string
815      */
816     protected function _encodeAccountId($accountId)
817     {
818         return $accountId;
819     }
820     
821     /**
822      * convert binary id to plain text id
823      * 
824      * @param  string  $groupId
825      * @return string
826      */
827     protected function _encodeGroupId($groupId)
828     {
829         return $groupId;
830     }
831     
832     /**
833      * returns a single account dn
834      *
835      * @param string $_accountId
836      * @return string
837      */
838     protected function _getAccountMetaData($_accountId)
839     {
840         return Tinebase_Helper::array_value(0, $this->_getAccountsMetaData(array($_accountId)));
841     }
842     
843     /**
844      * generates a new dn for a group
845      *
846      * @param  Tinebase_Model_Group $_group
847      * @return string
848      */
849     protected function _generateDn(Tinebase_Model_Group $_group)
850     {
851         $newDn = "cn={$_group->name},{$this->_options['groupsDn']}";
852         
853         return $newDn;
854     }
855     
856     /**
857      * generates a gidnumber
858      *
859      * @todo add a persistent registry which id has been generated lastly to
860      *       reduce amount of groupid to be transfered
861      * 
862      * @return int
863      */
864     protected function _generateGidNumber()
865     {
866         $allGidNumbers = array();
867         $gidNumber = null;
868         
869         $filter = Zend_Ldap_Filter::orFilter(
870             Zend_Ldap_Filter::equals('objectclass', 'posixgroup'),
871             Zend_Ldap_Filter::equals('objectclass', 'group')
872         );
873         
874         $groups = $this->getLdap()->search(
875             $filter, 
876             $this->_options['groupsDn'], 
877             Zend_Ldap::SEARCH_SCOPE_SUB, 
878             array('gidnumber')
879         );
880         
881         foreach ($groups as $groupData) {
882             $allGidNumbers[] = $groupData['gidnumber'][0];
883         }
884         sort($allGidNumbers);
885         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . "  Existing gidnumbers " . print_r($allGidNumbers, true));
886         
887         $numGroups = count($allGidNumbers);
888         if ($numGroups == 0 || $allGidNumbers[$numGroups-1] < $this->_options['minGroupId']) {
889             $gidNumber =  $this->_options['minGroupId'];
890         } elseif ($allGidNumbers[$numGroups-1] < $this->_options['maxGroupId']) {
891             $gidNumber = ++$allGidNumbers[$numGroups-1];
892         } elseif (count($allGidNumbers) < ($this->_options['maxGroupId'] - $this->_options['minGroupId'])) {
893             // maybe there is a gap
894             for($i = $this->_options['minGroupId']; $i <= $this->_options['maxGroupId']; $i++) {
895                 if (!in_array($i, $allGidNumbers)) {
896                     $gidNumber = $i;
897                     break;
898                 }
899             }
900         }
901         
902         if ($gidNumber === NULL) {
903             throw new Tinebase_Exception_NotImplemented('Max Group Id is reached');
904         }
905         
906         return $gidNumber;
907     }
908     
909     /**
910      * resolve groupid(for example ldap gidnumber) to uuid(for example ldap entryuuid)
911      *
912      * @param   string  $_groupId
913      * @return  string  the uuid for groupid
914      */
915     public function resolveSyncAbleGidToUUid($_groupId)
916     {
917         if ($this->isDisabledBackend()) {
918             throw new Tinebase_Exception_UnexpectedValue('backend is disabled');
919         }
920         
921         return $this->resolveGIdNumberToUUId($_groupId);
922     }
923     
924     /**
925      * resolve gidnumber to UUID(for example entryUUID) attribute
926      * 
927      * @param int $_gidNumber the gidnumber
928      * @return string 
929      */
930     public function resolveGIdNumberToUUId($_gidNumber)
931     {
932         if ($this->_groupUUIDAttribute == 'gidnumber') {
933             return $_gidNumber;
934         }
935         
936         if ($this->isDisabledBackend()) {
937             throw new Tinebase_Exception_UnexpectedValue('backend is disabled');
938         }
939         
940         $filter = Zend_Ldap_Filter::andFilter(
941             Zend_Ldap_Filter::string($this->_groupBaseFilter),
942             Zend_Ldap_Filter::equals('gidnumber', $_gidNumber)
943         );
944         
945         $groupId = $this->getLdap()->search(
946             $filter, 
947             $this->_options['groupsDn'], 
948             $this->_groupSearchScope, 
949             array($this->_groupUUIDAttribute)
950         )->getFirst();
951         
952         if ($groupId == null) {
953             throw new Tinebase_Exception_NotFound('LDAP group with (gidnumber=' . $_gidNumber . ') not found');
954         }
955         
956         return $groupId[$this->_groupUUIDAttribute][0];
957     }
958     
959     /**
960      * resolve UUID(for example entryUUID) to gidnumber
961      * 
962      * @param string $_uuid
963      * @return string
964      */
965     public function resolveUUIdToGIdNumber($_uuid)
966     {
967         if ($this->_groupUUIDAttribute == 'gidnumber') {
968             return $_uuid;
969         }
970         
971         if ($this->isDisabledBackend()) {
972             throw new Tinebase_Exception_UnexpectedValue('backend is disabled');
973         }
974         
975         $filter = Zend_Ldap_Filter::andFilter(
976             Zend_Ldap_Filter::string($this->_groupBaseFilter),
977             Zend_Ldap_Filter::equals($this->_groupUUIDAttribute, $this->_encodeGroupId($_uuid))
978         );
979         
980         $groupId = $this->getLdap()->search(
981             $filter, 
982             $this->_options['groupsDn'], 
983             $this->_groupSearchScope, 
984             array('gidnumber')
985         )->getFirst();
986         
987         return $groupId['gidnumber'][0];
988     }
989     
990     /**
991      * get groupmemberships of user from sync backend
992      * 
993      * @param   Tinebase_Model_User|string  $_userId
994      * @return  array  list of group ids
995      */
996     public function getGroupMembershipsFromSyncBackend($_userId)
997     {
998         if (!$this->isDisabledBackend()) {
999             $metaData = $this->_getUserMetaData($_userId);
1000             
1001             $filter = Zend_Ldap_Filter::andFilter(
1002                 Zend_Ldap_Filter::string($this->_groupBaseFilter),
1003                 Zend_Ldap_Filter::orFilter(
1004                     Zend_Ldap_Filter::equals('memberuid', Zend_Ldap::filterEscape($metaData['uid'][0])),
1005                     Zend_Ldap_Filter::equals('member',    Zend_Ldap::filterEscape($metaData['dn']))
1006                 )
1007             );
1008             
1009             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
1010                 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .' ldap search filter: ' . $filter);
1011             
1012             $groups = $this->getLdap()->search(
1013                 $filter, 
1014                 $this->_options['groupsDn'], 
1015                 $this->_groupSearchScope, 
1016                 array('cn', 'description', $this->_groupUUIDAttribute)
1017             );
1018             
1019             $memberships = array();
1020             
1021             foreach ($groups as $group) {
1022                 $memberships[] = $group[$this->_groupUUIDAttribute][0];
1023             }
1024         } else {
1025             $memberships = $this->getGroupMemberships($_userId);
1026             
1027             if (empty($memberships)) {
1028                 $memberships[] = Tinebase_Group::getInstance()->getDefaultGroup()->getId();
1029             }
1030         }
1031         
1032         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
1033             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .' group memberships: ' . print_r($memberships, TRUE));
1034         
1035         return $memberships;
1036     }
1037     
1038     /**
1039      * (non-PHPdoc)
1040      * @see tine20/Tinebase/Group/Interface/Syncable::mergeMissingProperties
1041      */
1042     public static function mergeMissingProperties($syncGroup, $sqlGroup)
1043     {
1044         // @TODO see ldap schema, email might be an attribute
1045         foreach (array('list_id', 'email', 'visibility') as $property) {
1046             $syncGroup->{$property} = $sqlGroup->{$property};
1047         }
1048     }
1049 }