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