Merge branch '2013.10' into 2014.11
[tine20] / tine20 / Tinebase / User / Ldap.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  User
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  */
11
12 /**
13  * User ldap backend
14  *
15  * @package     Tinebase
16  * @subpackage  User
17  */
18 class Tinebase_User_Ldap extends Tinebase_User_Sql implements Tinebase_User_Interface_SyncAble
19 {
20     /**
21      * @var array
22      */
23     protected $_options = array();
24
25     /**
26      * @var Tinebase_Ldap
27      */
28     protected $_ldap = NULL;
29
30     /**
31      * name of the ldap attribute which identifies a group uniquely
32      * for example gidNumber, entryUUID, objectGUID
33      * @var string
34      */
35     protected $_groupUUIDAttribute;
36
37     /**
38      * name of the ldap attribute which identifies a user uniquely
39      * for example uidNumber, entryUUID, objectGUID
40      * @var string
41      */
42     protected $_userUUIDAttribute;
43
44     /**
45      * mapping of ldap attributes to class properties
46      *
47      * @var array
48      */
49     protected $_rowNameMapping = array(
50         'accountDisplayName'        => 'displayname',
51         'accountFullName'           => 'cn',
52         'accountFirstName'          => 'givenname',
53         'accountLastName'           => 'sn',
54         'accountLoginName'          => 'uid',
55         'accountLastPasswordChange' => 'shadowlastchange',
56         'accountExpires'            => 'shadowexpire',
57         'accountPrimaryGroup'       => 'gidnumber',
58         'accountEmailAddress'       => 'mail',
59         'accountHomeDirectory'      => 'homedirectory',
60         'accountLoginShell'         => 'loginshell',
61         'accountStatus'             => 'shadowinactive'
62     );
63
64     /**
65      * objectclasses required by this backend
66      *
67      * @var array
68      */
69     protected $_requiredObjectClass = array(
70         'top',
71         'posixAccount',
72         'shadowAccount',
73         'inetOrgPerson',
74     );
75     
76     /**
77      * the base dn to work on (defaults to to userDn, but can also be machineDn)
78      *
79      * @var string
80      */
81     protected $_baseDn;
82
83     /**
84      * the basic group ldap filter (for example the objectclass)
85      *
86      * @var string
87      */
88     protected $_groupBaseFilter = 'objectclass=posixgroup';
89
90     /**
91      * the basic user ldap filter (for example the objectclass)
92      *
93      * @var string
94      */
95     protected $_userBaseFilter = 'objectclass=posixaccount';
96
97     /**
98      * the basic user search scope
99      *
100      * @var integer
101      */
102     protected $_userSearchScope = Zend_Ldap::SEARCH_SCOPE_SUB;
103
104     protected $_ldapPlugins = array();
105     
106     protected $_isReadOnlyBackend = false;
107     
108     /**
109      * used to save the last user properties from ldap backend
110      */
111     protected $_lastLdapProperties = array();
112     
113     /**
114      * the constructor
115      *
116      * @param  array  $_options  Options used in connecting, binding, etc.
117      * @throws Tinebase_Exception_Backend_Ldap
118      */
119     public function __construct(array $_options = array())
120     {
121         parent::__construct($_options);
122         
123         if(empty($_options['userUUIDAttribute'])) {
124             $_options['userUUIDAttribute'] = 'entryUUID';
125         }
126         if(empty($_options['groupUUIDAttribute'])) {
127             $_options['groupUUIDAttribute'] = 'entryUUID';
128         }
129         if(empty($_options['baseDn'])) {
130             $_options['baseDn'] = $_options['userDn'];
131         }
132         if(empty($_options['userFilter'])) {
133             $_options['userFilter'] = 'objectclass=posixaccount';
134         }
135         if(empty($_options['userSearchScope'])) {
136             $_options['userSearchScope'] = Zend_Ldap::SEARCH_SCOPE_SUB;
137         }
138         if(empty($_options['groupFilter'])) {
139             $_options['groupFilter'] = 'objectclass=posixgroup';
140         }
141
142         if (isset($_options['requiredObjectClass'])) {
143             $this->_requiredObjectClass = (array)$_options['requiredObjectClass'];
144         }
145         if ((isset($_options['readonly']) || array_key_exists('readonly', $_options))) {
146             $this->_isReadOnlyBackend = (bool)$_options['readonly'];
147         }
148         if ((isset($_options['ldap']) || array_key_exists('ldap', $_options))) {
149             $this->_ldap = $_options['ldap'];
150         }
151         
152         $this->_options = $_options;
153         
154         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
155             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " Registering " . print_r($this->_options, true));
156         
157         $this->_userUUIDAttribute  = strtolower($this->_options['userUUIDAttribute']);
158         $this->_groupUUIDAttribute = strtolower($this->_options['groupUUIDAttribute']);
159         $this->_baseDn             = $this->_options['baseDn'];
160         $this->_userBaseFilter     = $this->_options['userFilter'];
161         $this->_userSearchScope    = $this->_options['userSearchScope'];
162         $this->_groupBaseFilter    = $this->_options['groupFilter'];
163
164         $this->_rowNameMapping['accountId'] = $this->_userUUIDAttribute;
165         
166         if (! $this->_ldap instanceof Tinebase_Ldap) {
167             $this->_ldap = new Tinebase_Ldap($this->_options);
168             try {
169                 $this->_ldap->bind();
170             } catch (Zend_Ldap_Exception $zle) {
171                 // @todo move this to Tinebase_Ldap?
172                 throw new Tinebase_Exception_Backend_Ldap('Could not bind to LDAP: ' . $zle->getMessage());
173             }
174         }
175         
176         foreach ($this->_plugins as $plugin) {
177             if ($plugin instanceof Tinebase_User_Plugin_LdapInterface) {
178                 $this->registerLdapPlugin($plugin);
179             }
180         }
181     }
182
183     /**
184      * register ldap plugin
185      * 
186      * @param Tinebase_User_Plugin_LdapInterface $plugin
187      */
188     public function registerLdapPlugin(Tinebase_User_Plugin_LdapInterface $plugin)
189     {
190         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
191             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Registering " . get_class($plugin) . ' LDAP plugin.');
192         
193         $plugin->setLdap($this->_ldap);
194         $this->_ldapPlugins[] = $plugin;
195     }
196     
197     /**
198      * get list of users
199      *
200      * @param string $_filter
201      * @param string $_sort
202      * @param string $_dir
203      * @param int $_start
204      * @param int $_limit
205      * @param string $_accountClass the type of subclass for the Tinebase_Record_RecordSet to return
206      * @return Tinebase_Record_RecordSet with record class Tinebase_Model_User
207      */
208     public function getUsersFromSyncBackend($_filter = NULL, $_sort = NULL, $_dir = 'ASC', $_start = NULL, $_limit = NULL, $_accountClass = 'Tinebase_Model_User')
209     {
210         $filter = $this->_getBaseFilter();
211
212         if (!empty($_filter)) {
213             $filter = $filter->addFilter(Zend_Ldap_Filter::orFilter(
214                 Zend_Ldap_Filter::contains($this->_rowNameMapping['accountFirstName'], Zend_Ldap::filterEscape($_filter)),
215                 Zend_Ldap_Filter::contains($this->_rowNameMapping['accountLastName'], Zend_Ldap::filterEscape($_filter)),
216                 Zend_Ldap_Filter::contains($this->_rowNameMapping['accountLoginName'], Zend_Ldap::filterEscape($_filter))
217             ));
218         }
219
220         $accounts = $this->_ldap->search(
221             $filter,
222             $this->_baseDn,
223             $this->_userSearchScope,
224             array_values($this->_rowNameMapping),
225             $_sort !== null ? $this->_rowNameMapping[$_sort] : null
226         );
227         
228         $result = new Tinebase_Record_RecordSet($_accountClass);
229
230         // nothing to be done anymore
231         if (count($accounts) == 0) {
232             return $result;
233         }
234
235         foreach ($accounts as $account) {
236             $accountObject = $this->_ldap2User($account, $_accountClass);
237             
238             if ($accountObject) {
239                 $result->addRecord($accountObject);
240             }
241
242         }
243
244         return $result;
245
246         // @todo implement limit, start, dir and status
247 //         $select = $this->_getUserSelectObject()
248 //             ->limit($_limit, $_start);
249
250 //         if ($_sort !== NULL) {
251 //             $select->order($this->rowNameMapping[$_sort] . ' ' . $_dir);
252 //         }
253
254 //         // return only active users, when searching for simple users
255 //         if ($_accountClass == 'Tinebase_Model_User') {
256 //             $select->where($this->_db->quoteInto($this->_db->quoteIdentifier('status') . ' = ?', 'enabled'));
257 //         }
258     }
259     
260     /**
261      * returns user base filter
262      * 
263      * @return Zend_Ldap_Filter_And
264      */
265     protected function _getBaseFilter()
266     {
267         return Zend_Ldap_Filter::andFilter(
268             Zend_Ldap_Filter::string($this->_userBaseFilter)
269         );
270     }
271     
272     /**
273      * search for user attributes
274      * 
275      * @param array $attributes
276      * @return array
277      * 
278      * @todo allow multi value attributes
279      * @todo generalize this for usage in other Tinebase_User_Ldap fns?
280      */
281     public function getUserAttributes($attributes)
282     {
283         $ldapCollection = $this->_ldap->search(
284             $this->_getBaseFilter(),
285             $this->_baseDn,
286             $this->_userSearchScope,
287             $attributes
288         );
289         
290         $result = array();
291         foreach ($ldapCollection as $data) {
292             $row = array('dn' => $data['dn']);
293             foreach ($attributes as $key) {
294                 $lowerKey = strtolower($key);
295                 if (isset($data[$lowerKey]) && isset($data[$lowerKey][0])) {
296                     $row[$key] = $data[$lowerKey][0];
297                 }
298             }
299             $result[] = $row;
300         }
301         
302         return (array)$result;
303     }
304     
305     /**
306      * fetch LDAP backend 
307      * 
308      * @return Tinebase_Ldap
309      */
310     public function getLdap()
311     {
312         return $this->_ldap;
313     }
314
315     /**
316      * get user by login name
317      *
318      * @param   string  $_property
319      * @param   string  $_accountId
320      * @return Tinebase_Model_User the user object
321      */
322     public function getUserByPropertyFromSyncBackend($_property, $_accountId, $_accountClass = 'Tinebase_Model_User')
323     {
324         if (!(isset($this->_rowNameMapping[$_property]) || array_key_exists($_property, $this->_rowNameMapping))) {
325             throw new Tinebase_Exception_NotFound("can't get user by property $_property. property not supported by ldap backend.");
326         }
327
328         $ldapEntry = $this->_getLdapEntry($_property, $_accountId);
329         
330         $user = $this->_ldap2User($ldapEntry, $_accountClass);
331         
332         // append data from ldap plugins
333         foreach ($this->_ldapPlugins as $class => $plugin) {
334             $plugin->inspectGetUserByProperty($user, $ldapEntry);
335         }
336         
337         return $user;
338     }
339
340     /**
341      * set the password for given account
342      *
343      * @param   string  $_userId
344      * @param   string  $_password
345      * @param   bool    $_encrypt encrypt password
346      * @param   bool    $_mustChange
347      * @return  void
348      * @throws  Tinebase_Exception_InvalidArgument
349      */
350     public function setPassword($_userId, $_password, $_encrypt = TRUE, $_mustChange = null)
351     {
352         if ($this->_isReadOnlyBackend) {
353             return;
354         }
355         
356         $user = $_userId instanceof Tinebase_Model_FullUser ? $_userId : $this->getFullUserById($_userId);
357         
358         $this->checkPasswordPolicy($_password, $user);
359         
360         $metaData = $this->_getMetaData($user);
361
362         $encryptionType = isset($this->_options['pwEncType']) ? $this->_options['pwEncType'] : Tinebase_User_Abstract::ENCRYPT_SSHA;
363         $userpassword = ($_encrypt && $encryptionType !== Tinebase_User_Abstract::ENCRYPT_PLAIN)
364             ? Hash_Password::generate($encryptionType, $_password)
365             : $_password;
366         
367         $ldapData = array(
368             'userpassword'     => $userpassword,
369             'shadowlastchange' => floor(Tinebase_DateTime::now()->getTimestamp() / 86400)
370         );
371
372         foreach ($this->_ldapPlugins as $plugin) {
373             $plugin->inspectSetPassword($user, $_password, $_encrypt, $_mustChange, $ldapData);
374         }
375
376         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $dn: ' . $metaData['dn']);
377         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $ldapData: ' . print_r($ldapData, true));
378
379         $this->_ldap->update($metaData['dn'], $ldapData);
380         
381         // update last modify timestamp in sql backend too
382         $values = array(
383             'last_password_change' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
384         );
385         
386         $where = array(
387             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId())
388         );
389         
390         $this->_db->update(SQL_TABLE_PREFIX . 'accounts', $values, $where);
391         
392         $this->_setPluginsPassword($user->getId(), $_password, $_encrypt);
393     }
394
395     /**
396      * update user status (enabled or disabled)
397      *
398      * @param   mixed   $_accountId
399      * @param   string  $_status
400      */
401     public function setStatusInSyncBackend($_accountId, $_status)
402     {
403         if ($this->_isReadOnlyBackend) {
404             return;
405         }
406         
407         $metaData = $this->_getMetaData($_accountId);
408
409         if ($_status == 'disabled') {
410             $ldapData = array(
411                 'shadowMax'      => 1,
412                 'shadowInactive' => 1
413             );
414         } else {
415             $ldapData = array(
416                 'shadowMax'      => 999999,
417                 'shadowInactive' => array()
418             );
419         }
420
421         foreach ($this->_ldapPlugins as $plugin) {
422             $plugin->inspectStatus($_status, $ldapData);
423         }
424
425         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " {$metaData['dn']}  $ldapData: " . print_r($ldapData, true));
426
427         $this->_ldap->update($metaData['dn'], $ldapData);
428     }
429
430     /**
431      * sets/unsets expiry date in ldap backend
432      *
433      * expiryDate is the number of days since Jan 1, 1970
434      *
435      * @param   mixed      $_accountId
436      * @param   Tinebase_DateTime  $_expiryDate
437      */
438     public function setExpiryDateInSyncBackend($_accountId, $_expiryDate)
439     {
440         if ($this->_isReadOnlyBackend) {
441             return;
442         }
443         
444         $metaData = $this->_getMetaData($_accountId);
445
446         if ($_expiryDate instanceof DateTime) {
447             // days since Jan 1, 1970
448             $ldapData = array('shadowexpire' => floor($_expiryDate->getTimestamp() / 86400));
449         } else {
450             $ldapData = array('shadowexpire' => array());
451         }
452
453         foreach ($this->_ldapPlugins as $plugin) {
454             $plugin->inspectExpiryDate($_expiryDate, $ldapData);
455         }
456
457         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " {$metaData['dn']}  $ldapData: " . print_r($ldapData, true));
458
459         $this->_ldap->update($metaData['dn'], $ldapData);
460     }
461
462     /**
463      * updates an existing user
464      *
465      * @todo check required objectclasses?
466      *
467      * @param Tinebase_Model_FullUser $_account
468      * @return Tinebase_Model_FullUser
469      */
470     public function updateUserInSyncBackend(Tinebase_Model_FullUser $_account)
471     {
472         if ($this->_isReadOnlyBackend) {
473             return $_account;
474         }
475         
476         $ldapEntry = $this->_getLdapEntry('accountId', $_account);
477         
478         $ldapData = $this->_user2ldap($_account, $ldapEntry);
479         
480         foreach ($this->_ldapPlugins as $plugin) {
481             $plugin->inspectUpdateUser($_account, $ldapData, $ldapEntry);
482         }
483         
484         // no need to update this attribute, it's not allowed to change and even might not be updateable
485         unset($ldapData[$this->_userUUIDAttribute]);
486         
487         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  $dn: ' . $ldapEntry['dn']);
488         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  $ldapData: ' . print_r($ldapData, true));
489         
490         $this->_ldap->update($ldapEntry['dn'], $ldapData);
491         
492         $dn = Zend_Ldap_Dn::factory($ldapEntry['dn'], null);
493         $rdn = $dn->getRdn();
494         
495         // do we need to rename the entry?
496         if (isset($ldapData[key($rdn)]) && $rdn[key($rdn)] != $ldapData[key($rdn)]) {
497             $groupsBackend = Tinebase_Group::factory(Tinebase_Group::LDAP);
498             
499             // get the current groupmemberships
500             $memberships = $groupsBackend->getGroupMembershipsFromSyncBackend($_account);
501             
502             // remove the user from current groups, because the dn/uid has changed
503             foreach ($memberships as $groupId) {
504                 $groupsBackend->removeGroupMemberInSyncBackend($groupId, $_account);
505             }
506             
507             $newDN = $this->_generateDn($_account);
508             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  rename ldap entry to: ' . $newDN);
509             $this->_ldap->rename($dn, $newDN);
510             
511             // add the user to current groups again
512             foreach ($memberships as $groupId) {
513                 $groupsBackend->addGroupMemberInSyncBackend($groupId, $_account);
514             }
515         }
516         
517         // refetch user from ldap backend
518         $user = $this->getUserByPropertyFromSyncBackend('accountId', $_account, 'Tinebase_Model_FullUser');
519
520         return $user;
521     }
522
523     /**
524      * add an user
525      * 
526      * @param   Tinebase_Model_FullUser  $user
527      * @return  Tinebase_Model_FullUser|null
528      */
529     public function addUserToSyncBackend(Tinebase_Model_FullUser $user)
530     {
531         if ($this->_isReadOnlyBackend) {
532             return null;
533         }
534         
535         $ldapData = $this->_user2ldap($user);
536
537         $ldapData['uidnumber'] = $this->_generateUidNumber();
538         $ldapData['objectclass'] = $this->_requiredObjectClass;
539
540         foreach ($this->_ldapPlugins as $plugin) {
541             $plugin->inspectAddUser($user, $ldapData);
542         }
543
544         $dn = $this->_generateDn($user);
545         
546         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
547             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  ldapData: ' . print_r($ldapData, true));
548         
549         $this->_ldap->add($dn, $ldapData);
550
551         $userId = $this->_ldap->getEntry($dn, array($this->_userUUIDAttribute));
552
553         $userId = $userId[$this->_userUUIDAttribute][0];
554
555         $user = $this->getUserByPropertyFromSyncBackend('accountId', $userId, 'Tinebase_Model_FullUser');
556
557         return $user;
558     }
559
560     /**
561      * delete an user in ldap backend
562      *
563      * @param int $_userId
564      */
565     public function deleteUserInSyncBackend($_userId)
566     {
567         if ($this->_isReadOnlyBackend) {
568             return;
569         }
570         
571         try {
572             $metaData = $this->_getMetaData($_userId);
573     
574             if (! empty($metaData['dn'])) {
575                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
576                     . ' delete user ' . $metaData['dn'] .' from sync backend (LDAP)');
577                 $this->_ldap->delete($metaData['dn']);
578             }
579         } catch (Tinebase_Exception_NotFound $tenf) {
580             if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__
581                 . ' user not found in sync backend: ' . $_userId);
582         }
583     }
584
585     /**
586      * delete multiple users from ldap only
587      *
588      * @param array $_accountIds
589      */
590     public function deleteUsersInSyncBackend(array $_accountIds)
591     {
592         if ($this->_isReadOnlyBackend) {
593             return;
594         }
595         
596         foreach ($_accountIds as $accountId) {
597             $this->deleteUserInSyncBackend($accountId);
598         }
599     }
600
601     /**
602      * return ldap entry of user
603      * 
604      * @param string $_uid
605      * @return array
606      */
607     protected function _getLdapEntry($_property, $_userId)
608     {
609         switch($_property) {
610             case 'accountId':
611                 $value = $this->_encodeAccountId(Tinebase_Model_User::convertUserIdToInt($_userId));
612                 break;
613             default:
614                 $value = Zend_Ldap::filterEscape($_userId);
615                 break;
616         }
617         
618         $filter = Zend_Ldap_Filter::andFilter(
619             Zend_Ldap_Filter::string($this->_userBaseFilter),
620             Zend_Ldap_Filter::equals($this->_rowNameMapping[$_property], $value)
621         );
622         
623         $attributes = array_values($this->_rowNameMapping);
624         foreach ($this->_ldapPlugins as $plugin) {
625             $attributes = array_merge($attributes, $plugin->getSupportedAttributes());
626         }
627         $attributes[] = 'objectclass';
628         $attributes[] = 'uidnumber';
629         $attributes[] = 'useraccountcontrol';
630         
631         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
632             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' filter ' . $filter);
633         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
634             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' requested attributes ' . print_r($attributes, true));
635         
636         $accounts = $this->_ldap->search(
637             $filter,
638             $this->_baseDn,
639             $this->_userSearchScope,
640             $attributes
641         );
642         
643         if (count($accounts) !== 1) {
644             throw new Tinebase_Exception_NotFound('User with ' . $_property . ' =  ' . $value . ' not found.');
645         }
646         
647         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
648             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' current ldap values ' . print_r($accounts->getFirst(), true));
649         
650         return $accounts->getFirst();
651     }
652     
653     /**
654      * get metatada of existing user
655      *
656      * @param  string  $_userId
657      * @return array
658      */
659     protected function _getMetaData($_userId)
660     {
661         $userId = $this->_encodeAccountId(Tinebase_Model_User::convertUserIdToInt($_userId));
662
663         $filter = Zend_Ldap_Filter::equals(
664             $this->_rowNameMapping['accountId'], $userId
665         );
666
667         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
668             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
669                 . ' fetch meta data - filter: ' . $filter);
670
671         $result = $this->_ldap->search(
672             $filter,
673             $this->_baseDn,
674             $this->_userSearchScope
675         );
676
677         if (count($result) !== 1) {
678             throw new Tinebase_Exception_NotFound("user with userid $_userId not found");
679         }
680
681         return $result->getFirst();
682     }
683
684     /**
685      * generates dn for new user
686      *
687      * @param  Tinebase_Model_FullUser $_account
688      * @return string
689      */
690     protected function _generateDn(Tinebase_Model_FullUser $_account)
691     {
692         $baseDn = $this->_baseDn;
693
694         $uidProperty = array_search('uid', $this->_rowNameMapping);
695         $newDn = "uid={$_account->$uidProperty},{$baseDn}";
696
697         return $newDn;
698     }
699     
700     /**
701      * generates a uidnumber
702      *
703      * @todo add a persistent registry which id has been generated lastly to
704      *       reduce amount of userid to be transfered
705      *
706      * @return int
707      */
708     protected function _generateUidNumber()
709     {
710         $allUidNumbers = array();
711         $uidNumber = null;
712
713         $filter = Zend_Ldap_Filter::equals(
714             'objectclass', 'posixAccount'
715         );
716
717         $accounts = $this->_ldap->search(
718             $filter,
719             $this->_options['userDn'],
720             Zend_Ldap::SEARCH_SCOPE_SUB,
721             array('uidnumber')
722         );
723
724         foreach ($accounts as $userData) {
725             $allUidNumbers[$userData['uidnumber'][0]] = $userData['uidnumber'][0];
726         }
727
728         // fetch also the uidnumbers of machine accounts, if needed
729         // @todo move this to samba plugin
730         if (isset(Tinebase_Core::getConfig()->samba) && Tinebase_Core::getConfig()->samba->get('manageSAM', FALSE) == true) {
731             $accounts = $this->_ldap->search(
732                 $filter,
733                 Tinebase_Core::getConfig()->samba->get('machineDn'),
734                 Zend_Ldap::SEARCH_SCOPE_SUB,
735                 array('uidnumber')
736             );
737
738             foreach ($accounts as $userData) {
739                 $allUidNumbers[$userData['uidnumber'][0]] = $userData['uidnumber'][0];
740             }
741         }
742         sort($allUidNumbers);
743
744         #if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . "  Existing uidnumbers " . print_r($allUidNumbers, true));
745         $minUidNumber = isset($this->_options['minUserId']) ? $this->_options['minUserId'] : 1000;
746         $maxUidNumber = isset($this->_options['maxUserId']) ? $this->_options['maxUserId'] : 65000;
747
748         $numUsers = count($allUidNumbers);
749         if ($numUsers == 0 || $allUidNumbers[$numUsers-1] < $minUidNumber) {
750             $uidNumber = $minUidNumber;
751         } elseif ($allUidNumbers[$numUsers-1] < $maxUidNumber) {
752             $uidNumber = ++$allUidNumbers[$numUsers-1];
753         } elseif (count($allUidNumbers) < ($maxUidNumber - $minUidNumber)) {
754             // maybe there is a gap
755             for($i = $minUidNumber; $i <= $maxUidNumber; $i++) {
756                 if (!in_array($i, $allUidNumbers)) {
757                     $uidNumber = $i;
758                     break;
759                 }
760             }
761         }
762
763         if ($uidNumber === NULL) {
764             throw new Tinebase_Exception_NotImplemented('Max User Id is reached');
765         }
766
767         return $uidNumber;
768     }
769
770     /**
771      * return contact information for user
772      *
773      * @param  Tinebase_Model_FullUser    $_user
774      * @param  Addressbook_Model_Contact  $_contact
775      */
776     public function updateContactFromSyncBackend(Tinebase_Model_FullUser $_user, Addressbook_Model_Contact $_contact)
777     {
778         $userData = $this->_getMetaData($_user);
779
780         $userData = $this->_ldap->getEntry($userData['dn']);
781         
782         $this->_ldap2Contact($userData, $_contact);
783         
784         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
785             . " Synced user object: " . print_r($_contact->toArray(), true));
786     }
787     
788     /**
789      * update contact data(first name, last name, ...) of user
790      * 
791      * @param Addressbook_Model_Contact $contact
792      * @todo implement logic
793      */
794     public function updateContactInSyncBackend($_contact)
795     {
796         
797     }
798     
799     /**
800      * Returns a user obj with raw data from ldap
801      *
802      * @param array $_userData
803      * @param string $_accountClass
804      * @return Tinebase_Record_Abstract
805      */
806     protected function _ldap2User(array $_userData, $_accountClass)
807     {
808         $errors = false;
809         
810         $this->_lastLdapProperties = $_userData;
811         
812         foreach ($_userData as $key => $value) {
813             if (is_int($key)) {
814                 continue;
815             }
816             $keyMapping = array_search($key, $this->_rowNameMapping);
817             if ($keyMapping !== FALSE) {
818                 switch($keyMapping) {
819                     case 'accountLastPasswordChange':
820                     case 'accountExpires':
821                         $shadowExpire = $value[0];
822                         if ($shadowExpire < 0) {
823                             // account does not expire
824                             $accountArray[$keyMapping] = null;
825                         } else {
826                             $accountArray[$keyMapping] = new Tinebase_DateTime(($shadowExpire < 100000) ? $shadowExpire * 86400 : $shadowExpire);
827                         }
828                         break;
829                         
830                     case 'accountStatus':
831                         if ((isset($_userData['shadowmax']) || array_key_exists('shadowmax', $_userData)) && (isset($_userData['shadowinactive']) || array_key_exists('shadowinactive', $_userData))) {
832                             $lastChange = (isset($_userData['shadowlastchange']) || array_key_exists('shadowlastchange', $_userData)) ? $_userData['shadowlastchange'] : 0;
833                             if (($lastChange + $_userData['shadowmax'] + $_userData['shadowinactive']) * 86400 <= Tinebase_DateTime::now()->getTimestamp()) {
834                                 $accountArray[$keyMapping] = 'enabled';
835                             } else {
836                                 $accountArray[$keyMapping] = 'disabled';
837                             }
838                         } else {
839                             $accountArray[$keyMapping] = 'enabled';
840                         }
841                         break;
842
843                     case 'accountId':
844                         $accountArray[$keyMapping] = $this->_decodeAccountId($value[0]);
845                         
846                         break;
847                         
848                     default:
849                         $accountArray[$keyMapping] = $value[0];
850                         break;
851                 }
852             }
853         }
854
855         if (empty($accountArray['accountLastName']) && !empty($accountArray['accountFullName'])) {
856             $accountArray['accountLastName'] = $accountArray['accountFullName'];
857         }
858         if ($errors) {
859             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not instantiate account object for ldap user ' . print_r($_userData, 1));
860             $accountObject = null;
861         } else {
862             $accountObject = new $_accountClass($accountArray, TRUE);
863         }
864
865         return $accountObject;
866     }
867     
868     /**
869      * returns properties of last user fetched from sync backend
870      * 
871      * @return array
872      */
873     public function getLastUserProperties()
874     {
875         return $this->_lastLdapProperties;
876     }
877     
878     /**
879      * helper function to be overwriten in subclasses
880      * 
881      * @param  string  $accountId
882      * @return string
883      */
884     protected function _decodeAccountId($accountId)
885     {
886         return $accountId;
887     }
888
889     /**
890      * helper function to be overwriten in subclasses
891      * 
892      * @param  string  $accountId
893      * @return string
894      */
895     protected function _encodeAccountId($accountId)
896     {
897         return $accountId;
898     }
899
900     /**
901      * parse ldap result set and update Addressbook_Model_Contact
902      *
903      * @param array                      $_userData
904      * @param Addressbook_Model_Contact  $_contact
905      */
906     protected function _ldap2Contact($_userData, Addressbook_Model_Contact $_contact)
907     {
908         $rowNameMapping = array(
909             'bday'                  => 'birthdate',
910             'tel_cell'              => 'mobile',
911             'tel_work'              => 'telephonenumber',
912             'tel_home'              => 'homephone',
913             'tel_fax'               => 'facsimiletelephonenumber',
914             'org_name'              => 'o',
915             'org_unit'              => 'ou',
916             'email_home'            => 'mozillasecondemail',
917             'jpegphoto'             => 'jpegphoto',
918             'adr_two_locality'      => 'mozillahomelocalityname',
919             'adr_two_postalcode'    => 'mozillahomepostalcode',
920             'adr_two_region'        => 'mozillahomestate',
921             'adr_two_street'        => 'mozillahomestreet',
922             'adr_one_locality'      => 'l',
923             'adr_one_postalcode'    => 'postalcode',
924             'adr_one_street'        => 'street',
925             'adr_one_region'        => 'st',
926         );
927
928         foreach ($_userData as $key => $value) {
929             if (is_int($key)) {
930                 continue;
931             }
932
933             $keyMapping = array_search($key, $rowNameMapping);
934
935             if ($keyMapping !== FALSE) {
936                 switch($keyMapping) {
937                     case 'bday':
938                         $_contact->$keyMapping = Tinebase_DateTime::createFromFormat('Y-m-d', $value[0]);
939                         break;
940                     default:
941                         $_contact->$keyMapping = $value[0];
942                         break;
943                 }
944             }
945         }
946     }
947
948     /**
949      * returns array of ldap data
950      *
951      * @param  Tinebase_Model_FullUser $_user
952      * @return array
953      */
954     protected function _user2ldap(Tinebase_Model_FullUser $_user, array $_ldapEntry = array())
955     {
956         $ldapData = array();
957
958         foreach ($_user as $key => $value) {
959             $ldapProperty = (isset($this->_rowNameMapping[$key]) || array_key_exists($key, $this->_rowNameMapping)) ? $this->_rowNameMapping[$key] : false;
960
961             if ($ldapProperty) {
962                 switch ($key) {
963                     case 'accountLastPasswordChange':
964                         // field is readOnly
965                         break;
966                     case 'accountExpires':
967                         $ldapData[$ldapProperty] = $value instanceof DateTime ? floor($value->getTimestamp() / 86400) : array();
968                         break;
969                     case 'accountStatus':
970                         if ($value == 'enabled') {
971                             $ldapData['shadowMax']      = 999999;
972                             $ldapData['shadowInactive'] = array();
973                         } else {
974                             $ldapData['shadowMax']      = 1;
975                             $ldapData['shadowInactive'] = 1;
976                         }
977                         break;
978                     case 'accountPrimaryGroup':
979                         $ldapData[$ldapProperty] = Tinebase_Group::getInstance()->resolveUUIdToGIdNumber($value);
980                         break;
981                     default:
982                         $ldapData[$ldapProperty] = $value;
983                         break;
984                 }
985             }
986         }
987
988         // homedir is an required attribute
989         if (empty($ldapData['homedirectory'])) {
990             $ldapData['homedirectory'] = '/dev/null';
991         }
992         
993         $ldapData['objectclass'] = isset($_ldapEntry['objectclass']) ? $_ldapEntry['objectclass'] : array();
994         
995         // check if user has all required object classes. This is needed
996         // when updating users which where created using different requirements
997         foreach ($this->_requiredObjectClass as $className) {
998             if (! in_array($className, $ldapData['objectclass'])) {
999                 // merge all required classes at once
1000                 $ldapData['objectclass'] = array_unique(array_merge($ldapData['objectclass'], $this->_requiredObjectClass));
1001                 break;
1002             }
1003         }
1004         
1005         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' LDAP data ' . print_r($ldapData, true));
1006         
1007         return $ldapData;
1008     }
1009
1010     public function resolveUIdNumberToUUId($_uidNumber)
1011     {
1012         if ($this->_userUUIDAttribute == 'uidnumber') {
1013             return $_uidNumber;
1014         }
1015
1016         $filter = Zend_Ldap_Filter::equals(
1017             'uidnumber', Zend_Ldap::filterEscape($_uidNumber)
1018         );
1019
1020         $userId = $this->_ldap->search(
1021             $filter,
1022             $this->_baseDn,
1023             $this->_userSearchScope,
1024             array($this->_userUUIDAttribute)
1025         )->getFirst();
1026
1027         return $userId[$this->_userUUIDAttribute][0];
1028     }
1029
1030     /**
1031      * resolve UUID(for example entryUUID) to uidnumber
1032      *
1033      * @param string $_uuid
1034      * @return string
1035      */
1036     public function resolveUUIdToUIdNumber($_uuid)
1037     {
1038         if ($this->_userUUIDAttribute == 'uidnumber') {
1039             return $_uuid;
1040         }
1041
1042         $filter = Zend_Ldap_Filter::equals(
1043             $this->_userUUIDAttribute, $this->_encodeAccountId($_uuid)
1044         );
1045
1046         $groupId = $this->_ldap->search(
1047             $filter,
1048             $this->_options['userDn'],
1049             $this->_userSearchScope,
1050             array('uidnumber')
1051         )->getFirst();
1052
1053         return $groupId['uidnumber'][0];
1054     }
1055 }