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-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      * 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         $ldapData = $this->_getUserStatusValues($_status);
409
410         foreach ($this->_ldapPlugins as $plugin) {
411             $plugin->inspectStatus($_status, $ldapData);
412         }
413
414         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
415             " {$metaData['dn']}  ldapData: " . print_r($ldapData, true));
416
417         $this->_ldap->update($metaData['dn'], $ldapData);
418     }
419
420     /**
421      * get LDAP user status values depending on tine20 status
422      *
423      * @param $status one of expired, enabled, disabled
424      * @return array
425      */
426     protected function _getUserStatusValues($status)
427     {
428         if ($status == Tinebase_Model_User::ACCOUNT_STATUS_DISABLED) {
429             $ldapData = array(
430                 'shadowMax'      => 1,
431                 'shadowInactive' => 1
432             );
433         } else {
434             $ldapData = array(
435                 'shadowMax'      => 999999,
436                 'shadowInactive' => array()
437             );
438             if ($status == Tinebase_Model_User::ACCOUNT_STATUS_ENABLED) {
439                 // remove expiry setting
440                 $ldapData['shadowexpire'] = array();
441             }
442         }
443
444         return $ldapData;
445     }
446
447     /**
448      * sets/unsets expiry date in ldap backend
449      *
450      * expiryDate is the number of days since Jan 1, 1970
451      *
452      * @param   mixed      $_accountId
453      * @param   Tinebase_DateTime  $_expiryDate
454      */
455     public function setExpiryDateInSyncBackend($_accountId, $_expiryDate)
456     {
457         if ($this->_isReadOnlyBackend) {
458             return;
459         }
460         
461         $metaData = $this->_getMetaData($_accountId);
462
463         if ($_expiryDate instanceof DateTime) {
464             // days since Jan 1, 1970
465             $ldapData = array('shadowexpire' => floor($_expiryDate->getTimestamp() / 86400));
466         } else {
467             $ldapData = array('shadowexpire' => array());
468         }
469
470         foreach ($this->_ldapPlugins as $plugin) {
471             $plugin->inspectExpiryDate($_expiryDate, $ldapData);
472         }
473
474         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
475             . " {$metaData['dn']}  ldapData: " . print_r($ldapData, true));
476
477         $this->_ldap->update($metaData['dn'], $ldapData);
478     }
479
480     /**
481      * updates an existing user
482      *
483      * @todo check required objectclasses?
484      *
485      * @param Tinebase_Model_FullUser $_account
486      * @return Tinebase_Model_FullUser
487      */
488     public function updateUserInSyncBackend(Tinebase_Model_FullUser $_account)
489     {
490         if ($this->_isReadOnlyBackend) {
491             return $_account;
492         }
493         
494         $ldapEntry = $this->_getLdapEntry('accountId', $_account);
495         
496         $ldapData = $this->_user2ldap($_account, $ldapEntry);
497         
498         foreach ($this->_ldapPlugins as $plugin) {
499             $plugin->inspectUpdateUser($_account, $ldapData, $ldapEntry);
500         }
501         
502         // no need to update this attribute, it's not allowed to change and even might not be update-able
503         unset($ldapData[$this->_userUUIDAttribute]);
504         
505         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
506             . ' DN: ' . $ldapEntry['dn']);
507         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
508             . ' LDAP data: ' . print_r($ldapData, true));
509         
510         $this->_ldap->update($ldapEntry['dn'], $ldapData);
511         
512         $dn = Zend_Ldap_Dn::factory($ldapEntry['dn'], null);
513         $rdn = $dn->getRdn();
514         
515         // do we need to rename the entry?
516         if (isset($ldapData[key($rdn)]) && $rdn[key($rdn)] != $ldapData[key($rdn)]) {
517             $groupsBackend = Tinebase_Group::factory(Tinebase_Group::LDAP);
518             
519             // get the current group memberships
520             $memberships = $groupsBackend->getGroupMembershipsFromSyncBackend($_account);
521             
522             // remove the user from current groups, because the dn/uid has changed
523             foreach ($memberships as $groupId) {
524                 $groupsBackend->removeGroupMemberInSyncBackend($groupId, $_account);
525             }
526             
527             $newDN = $this->generateDn($_account);
528             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . '  rename ldap entry to: ' . $newDN);
529             $this->_ldap->rename($dn, $newDN);
530             
531             // add the user to current groups again
532             foreach ($memberships as $groupId) {
533                 $groupsBackend->addGroupMemberInSyncBackend($groupId, $_account);
534             }
535         }
536         
537         // refetch user from ldap backend
538         $user = $this->getUserByPropertyFromSyncBackend('accountId', $_account, 'Tinebase_Model_FullUser');
539
540         return $user;
541     }
542
543     /**
544      * add an user
545      * 
546      * @param   Tinebase_Model_FullUser  $user
547      * @return  Tinebase_Model_FullUser|null
548      */
549     public function addUserToSyncBackend(Tinebase_Model_FullUser $user)
550     {
551         if ($this->_isReadOnlyBackend) {
552             return null;
553         }
554         
555         $ldapData = $this->_user2ldap($user);
556
557         $ldapData['uidnumber'] = $this->_generateUidNumber();
558         $ldapData['objectclass'] = $this->_requiredObjectClass;
559
560         foreach ($this->_ldapPlugins as $plugin) {
561             $plugin->inspectAddUser($user, $ldapData);
562         }
563
564         $dn = $this->generateDn($user);
565         
566         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
567             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . '  ldapData: ' . print_r($ldapData, true));
568         
569         $this->_ldap->add($dn, $ldapData);
570
571         $userId = $this->_ldap->getEntry($dn, array($this->_userUUIDAttribute));
572
573         $userId = $userId[$this->_userUUIDAttribute][0];
574
575         $user = $this->getUserByPropertyFromSyncBackend('accountId', $userId, 'Tinebase_Model_FullUser');
576
577         return $user;
578     }
579
580     /**
581      * delete an user in ldap backend
582      *
583      * @param int $_userId
584      */
585     public function deleteUserInSyncBackend($_userId)
586     {
587         if ($this->_isReadOnlyBackend) {
588             return;
589         }
590         
591         try {
592             $metaData = $this->_getMetaData($_userId);
593     
594             if (! empty($metaData['dn'])) {
595                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
596                     . ' delete user ' . $metaData['dn'] .' from sync backend (LDAP)');
597                 $this->_ldap->delete($metaData['dn']);
598             }
599         } catch (Tinebase_Exception_NotFound $tenf) {
600             if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__
601                 . ' user not found in sync backend: ' . $_userId);
602         }
603     }
604
605     /**
606      * delete multiple users from ldap only
607      *
608      * @param array $_accountIds
609      */
610     public function deleteUsersInSyncBackend(array $_accountIds)
611     {
612         if ($this->_isReadOnlyBackend) {
613             return;
614         }
615         
616         foreach ($_accountIds as $accountId) {
617             $this->deleteUserInSyncBackend($accountId);
618         }
619     }
620
621     /**
622      * return ldap entry of user
623      * 
624      * @param string $_uid
625      * @return array
626      */
627     protected function _getLdapEntry($_property, $_userId)
628     {
629         switch($_property) {
630             case 'accountId':
631                 $value = $this->_encodeAccountId(Tinebase_Model_User::convertUserIdToInt($_userId));
632                 break;
633             default:
634                 $value = Zend_Ldap::filterEscape($_userId);
635                 break;
636         }
637         
638         $filter = Zend_Ldap_Filter::andFilter(
639             Zend_Ldap_Filter::string($this->_userBaseFilter),
640             Zend_Ldap_Filter::equals($this->_rowNameMapping[$_property], $value)
641         );
642         
643         $attributes = array_values($this->_rowNameMapping);
644         foreach ($this->_ldapPlugins as $plugin) {
645             $attributes = array_merge($attributes, $plugin->getSupportedAttributes());
646         }
647         $attributes[] = 'objectclass';
648         $attributes[] = 'uidnumber';
649         $attributes[] = 'useraccountcontrol';
650         // needed for account status handling (shadowmax: days after which password must be changed)
651         $attributes[] = 'shadowmax';
652
653         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
654             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' filter ' . $filter);
655         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
656             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' requested attributes ' . print_r($attributes, true));
657         
658         $accounts = $this->_ldap->search(
659             $filter,
660             $this->_baseDn,
661             $this->_userSearchScope,
662             $attributes
663         );
664         
665         if (count($accounts) !== 1) {
666             throw new Tinebase_Exception_NotFound('User with ' . $_property . ' =  ' . $value . ' not found.');
667         }
668         
669         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
670             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' current ldap values ' . print_r($accounts->getFirst(), true));
671         
672         return $accounts->getFirst();
673     }
674     
675     /**
676      * get metatada of existing user
677      *
678      * @param  string  $_userId
679      * @return array
680      */
681     protected function _getMetaData($_userId)
682     {
683         $userId = $this->_encodeAccountId(Tinebase_Model_User::convertUserIdToInt($_userId));
684
685         $filter = Zend_Ldap_Filter::equals(
686             $this->_rowNameMapping['accountId'], $userId
687         );
688
689         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
690             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
691                 . ' fetch meta data - filter: ' . $filter);
692
693         $result = $this->_ldap->search(
694             $filter,
695             $this->_baseDn,
696             $this->_userSearchScope
697         );
698
699         if (count($result) !== 1) {
700             throw new Tinebase_Exception_NotFound("user with userid $_userId not found");
701         }
702
703         return $result->getFirst();
704     }
705
706     /**
707      * generates dn for new user
708      *
709      * @param  Tinebase_Model_FullUser $_account
710      * @return string
711      */
712     public function generateDn(Tinebase_Model_FullUser $_account)
713     {
714         $baseDn = $this->_baseDn;
715         $uidProperty = array_search('uid', $this->_rowNameMapping);
716         $newDn = "uid={$_account->$uidProperty},{$baseDn}";
717
718         return $newDn;
719     }
720     
721     /**
722      * generates a uidnumber
723      *
724      * @todo add a persistent registry which id has been generated lastly to
725      *       reduce amount of userid to be transfered
726      *
727      * @return int
728      */
729     protected function _generateUidNumber()
730     {
731         $allUidNumbers = array();
732         $uidNumber = null;
733
734         $filter = Zend_Ldap_Filter::equals(
735             'objectclass', 'posixAccount'
736         );
737
738         $accounts = $this->_ldap->search(
739             $filter,
740             $this->_options['userDn'],
741             Zend_Ldap::SEARCH_SCOPE_SUB,
742             array('uidnumber')
743         );
744
745         foreach ($accounts as $userData) {
746             $allUidNumbers[$userData['uidnumber'][0]] = $userData['uidnumber'][0];
747         }
748
749         // fetch also the uidnumbers of machine accounts, if needed
750         // @todo move this to samba plugin
751         if (isset(Tinebase_Core::getConfig()->samba) && Tinebase_Core::getConfig()->samba->get('manageSAM', FALSE) == true) {
752             $accounts = $this->_ldap->search(
753                 $filter,
754                 Tinebase_Core::getConfig()->samba->get('machineDn'),
755                 Zend_Ldap::SEARCH_SCOPE_SUB,
756                 array('uidnumber')
757             );
758
759             foreach ($accounts as $userData) {
760                 $allUidNumbers[$userData['uidnumber'][0]] = $userData['uidnumber'][0];
761             }
762         }
763         sort($allUidNumbers);
764
765         #if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . "  Existing uidnumbers " . print_r($allUidNumbers, true));
766         $minUidNumber = isset($this->_options['minUserId']) ? $this->_options['minUserId'] : 1000;
767         $maxUidNumber = isset($this->_options['maxUserId']) ? $this->_options['maxUserId'] : 65000;
768
769         $numUsers = count($allUidNumbers);
770         if ($numUsers == 0 || $allUidNumbers[$numUsers-1] < $minUidNumber) {
771             $uidNumber = $minUidNumber;
772         } elseif ($allUidNumbers[$numUsers-1] < $maxUidNumber) {
773             $uidNumber = ++$allUidNumbers[$numUsers-1];
774         } elseif (count($allUidNumbers) < ($maxUidNumber - $minUidNumber)) {
775             // maybe there is a gap
776             for($i = $minUidNumber; $i <= $maxUidNumber; $i++) {
777                 if (!in_array($i, $allUidNumbers)) {
778                     $uidNumber = $i;
779                     break;
780                 }
781             }
782         }
783
784         if ($uidNumber === NULL) {
785             throw new Tinebase_Exception_NotImplemented('Max User Id is reached');
786         }
787
788         return $uidNumber;
789     }
790
791     /**
792      * return contact information for user
793      *
794      * @param  Tinebase_Model_FullUser    $_user
795      * @param  Addressbook_Model_Contact  $_contact
796      */
797     public function updateContactFromSyncBackend(Tinebase_Model_FullUser $_user, Addressbook_Model_Contact $_contact)
798     {
799         $userData = $this->_getMetaData($_user);
800
801         $userData = $this->_ldap->getEntry($userData['dn']);
802         
803         $this->_ldap2Contact($userData, $_contact);
804         
805         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
806             . " Synced user object: " . print_r($_contact->toArray(), true));
807     }
808     
809     /**
810      * update contact data(first name, last name, ...) of user
811      * 
812      * @param Addressbook_Model_Contact $contact
813      * @todo implement logic
814      */
815     public function updateContactInSyncBackend($_contact)
816     {
817         
818     }
819     
820     /**
821      * Returns a user obj with raw data from ldap
822      *
823      * @param array $_userData
824      * @param string $_accountClass
825      * @return Tinebase_Record_Abstract
826      */
827     protected function _ldap2User(array $_userData, $_accountClass)
828     {
829         $errors = false;
830         
831         $this->_lastLdapProperties = $_userData;
832         
833         foreach ($_userData as $key => $value) {
834             if (is_int($key)) {
835                 continue;
836             }
837             $keyMapping = array_search($key, $this->_rowNameMapping);
838             if ($keyMapping !== FALSE) {
839                 switch($keyMapping) {
840                     case 'accountLastPasswordChange':
841                     case 'accountExpires':
842                         $shadowExpire = $value[0];
843                         if ($shadowExpire < 0) {
844                             // account does not expire
845                             $accountArray[$keyMapping] = null;
846                         } else {
847                             $accountArray[$keyMapping] = new Tinebase_DateTime(($shadowExpire < 100000)
848                                 ? $shadowExpire * 86400
849                                 : $shadowExpire);
850                         }
851                         break;
852
853                     // shadowInactive
854                     case 'accountStatus':
855                         if ($this->_isUserDisabled($_userData)) {
856                             $accountArray[$keyMapping] = Tinebase_Model_User::ACCOUNT_STATUS_DISABLED;
857                         }
858                         break;
859
860                     case 'accountId':
861                         $accountArray[$keyMapping] = $this->_decodeAccountId($value[0]);
862                         break;
863                         
864                     default:
865                         $accountArray[$keyMapping] = $value[0];
866                         break;
867                 }
868             }
869         }
870
871         if (empty($accountArray['accountLastName']) && !empty($accountArray['accountFullName'])) {
872             $accountArray['accountLastName'] = $accountArray['accountFullName'];
873         }
874         if ($errors) {
875             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not instantiate account object for ldap user '
876                 . print_r($_userData, 1));
877             $accountObject = null;
878         } else {
879             $accountObject = new $_accountClass($accountArray, TRUE);
880         }
881
882         // normalize account status
883         if ($accountObject instanceof Tinebase_Model_FullUser) {
884             if (!isset($accountObject->accountStatus)) {
885                 $accountObject->accountStatus = Tinebase_Model_User::ACCOUNT_STATUS_ENABLED;
886             }
887             if ($accountObject->accountExpires &&
888                 $accountObject->accountExpires->isEarlier(Tinebase_DateTime::now()) &&
889                 $accountObject->accountStatus === Tinebase_Model_User::ACCOUNT_STATUS_ENABLED
890             ) {
891                 $accountObject->accountStatus = Tinebase_Model_User::ACCOUNT_STATUS_EXPIRED;
892             }
893         }
894
895         return $accountObject;
896     }
897
898     /**
899      * check if user is disabled in LDAP
900      *
901      * @param array $ldapData
902      * @return bool
903      *
904      * TODO fix/improve LDAP disabled user detection
905      */
906     protected function _isUserDisabled($ldapData)
907     {
908         if ((isset($ldapData['shadowmax']) || array_key_exists('shadowmax', $ldapData))) {
909             // FIXME this is very strange code!
910 //            $lastChange = (isset($ldapData['shadowlastchange']) || array_key_exists('shadowlastchange', $ldapData)) ? $ldapData['shadowlastchange'][0] : 0;
911 //            if (($lastChange + $ldapData['shadowmax'][0] + $ldapData['shadowinactive'][0]) * 86400
912 //                <= Tinebase_DateTime::now()->getTimestamp()
913 //            ) {
914 //                return false;
915 //            } else {
916 //                return true;
917 //            }
918
919             // this is what tine sets for disabled accounts
920             if (isset($ldapData['shadowmax'][0]) && $ldapData['shadowmax'][0] == 1 &&
921                 isset($ldapData['shadowinactive'][0]) && $ldapData['shadowinactive'][0] == 1
922             ) {
923                 return true;
924             }
925         }
926         return false;
927     }
928     
929     /**
930      * returns properties of last user fetched from sync backend
931      * 
932      * @return array
933      */
934     public function getLastUserProperties()
935     {
936         return $this->_lastLdapProperties;
937     }
938     
939     /**
940      * helper function to be overwritten in subclasses
941      * 
942      * @param  string  $accountId
943      * @return string
944      */
945     protected function _decodeAccountId($accountId)
946     {
947         return $accountId;
948     }
949
950     /**
951      * helper function to be overwritten in subclasses
952      * 
953      * @param  string  $accountId
954      * @return string
955      */
956     protected function _encodeAccountId($accountId)
957     {
958         return $accountId;
959     }
960
961     /**
962      * parse ldap result set and update Addressbook_Model_Contact
963      *
964      * @param array                      $_userData
965      * @param Addressbook_Model_Contact  $_contact
966      */
967     protected function _ldap2Contact($_userData, Addressbook_Model_Contact $_contact)
968     {
969         $rowNameMapping = array(
970             'bday'                  => 'birthdate',
971             'tel_cell'              => 'mobile',
972             'tel_work'              => 'telephonenumber',
973             'tel_home'              => 'homephone',
974             'tel_fax'               => 'facsimiletelephonenumber',
975             'org_name'              => 'o',
976             'org_unit'              => 'ou',
977             'email_home'            => 'mozillasecondemail',
978             'jpegphoto'             => 'jpegphoto',
979             'adr_two_locality'      => 'mozillahomelocalityname',
980             'adr_two_postalcode'    => 'mozillahomepostalcode',
981             'adr_two_region'        => 'mozillahomestate',
982             'adr_two_street'        => 'mozillahomestreet',
983             'adr_one_locality'      => 'l',
984             'adr_one_postalcode'    => 'postalcode',
985             'adr_one_street'        => 'street',
986             'adr_one_region'        => 'st',
987         );
988
989         $overwrittenFields = Tinebase_Config::getInstance()->get(Tinebase_Config::LDAP_OVERWRITE_CONTACT_FIELDS)->toArray();
990         foreach ($rowNameMapping as $tineKey => $ldapKey) {
991             if (isset($_userData[$ldapKey])) {
992                 switch ($tineKey) {
993                     case 'bday':
994                         $_contact->$tineKey = Tinebase_DateTime::createFromFormat('Y-m-d', $_userData[$ldapKey][0]);
995                         break;
996                     default:
997                         $_contact->$tineKey = $_userData[$ldapKey][0];
998                         break;
999                 }
1000             } else if (in_array($tineKey, $overwrittenFields)) {
1001                 // should empty values in ldap overwrite tine values
1002                 $_contact->$tineKey = '';
1003             }
1004         }
1005     }
1006
1007     /**
1008      * returns array of ldap data
1009      *
1010      * @param  Tinebase_Model_FullUser $_user
1011      * @return array
1012      */
1013     protected function _user2ldap(Tinebase_Model_FullUser $_user, array $_ldapEntry = array())
1014     {
1015         $ldapData = array();
1016
1017         foreach ($_user as $key => $value) {
1018             $ldapProperty = (isset($this->_rowNameMapping[$key]) || array_key_exists($key, $this->_rowNameMapping)) ? $this->_rowNameMapping[$key] : false;
1019
1020             if ($ldapProperty) {
1021                 switch ($key) {
1022                     case 'accountLastPasswordChange':
1023                         // field is readOnly
1024                         break;
1025                     case 'accountExpires':
1026                         $ldapData[$ldapProperty] = $value instanceof DateTime ? floor($value->getTimestamp() / 86400) : array();
1027                         break;
1028                     case 'accountStatus':
1029                         $ldapData = array_merge($ldapData, $this->_getUserStatusValues($value));
1030                         break;
1031                     case 'accountPrimaryGroup':
1032                         $ldapData[$ldapProperty] = Tinebase_Group::getInstance()->resolveUUIdToGIdNumber($value);
1033                         break;
1034                     default:
1035                         $ldapData[$ldapProperty] = $value;
1036                         break;
1037                 }
1038             }
1039         }
1040
1041         // homedir is an required attribute
1042         if (empty($ldapData['homedirectory'])) {
1043             $ldapData['homedirectory'] = '/dev/null';
1044         }
1045         
1046         $ldapData['objectclass'] = isset($_ldapEntry['objectclass']) ? $_ldapEntry['objectclass'] : array();
1047         
1048         // check if user has all required object classes. This is needed
1049         // when updating users which where created using different requirements
1050         foreach ($this->_requiredObjectClass as $className) {
1051             if (! in_array($className, $ldapData['objectclass'])) {
1052                 // merge all required classes at once
1053                 $ldapData['objectclass'] = array_unique(array_merge($ldapData['objectclass'], $this->_requiredObjectClass));
1054                 break;
1055             }
1056         }
1057         
1058         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' LDAP data ' . print_r($ldapData, true));
1059         
1060         return $ldapData;
1061     }
1062
1063     public function resolveUIdNumberToUUId($_uidNumber)
1064     {
1065         if ($this->_userUUIDAttribute == 'uidnumber') {
1066             return $_uidNumber;
1067         }
1068
1069         $filter = Zend_Ldap_Filter::equals(
1070             'uidnumber', Zend_Ldap::filterEscape($_uidNumber)
1071         );
1072
1073         $userId = $this->_ldap->search(
1074             $filter,
1075             $this->_baseDn,
1076             $this->_userSearchScope,
1077             array($this->_userUUIDAttribute)
1078         )->getFirst();
1079
1080         return $userId[$this->_userUUIDAttribute][0];
1081     }
1082
1083     /**
1084      * resolve UUID(for example entryUUID) to uidnumber
1085      *
1086      * @param string $_uuid
1087      * @return string
1088      */
1089     public function resolveUUIdToUIdNumber($_uuid)
1090     {
1091         if ($this->_userUUIDAttribute == 'uidnumber') {
1092             return $_uuid;
1093         }
1094
1095         $filter = Zend_Ldap_Filter::equals(
1096             $this->_userUUIDAttribute, $this->_encodeAccountId($_uuid)
1097         );
1098
1099         $groupId = $this->_ldap->search(
1100             $filter,
1101             $this->_options['userDn'],
1102             $this->_userSearchScope,
1103             array('uidnumber')
1104         )->getFirst();
1105
1106         return $groupId['uidnumber'][0];
1107     }
1108 }