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