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