Merge branch '2013.10' into 2014.11
[tine20] / tine20 / Tinebase / User.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-2013 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  */
11
12 /**
13  * User Class
14  *
15  * @package     Tinebase
16  * @subpackage  User
17  */
18 class Tinebase_User
19 {
20     /**
21      * backend constants
22      * 
23      * @var string
24      */
25     const ACTIVEDIRECTORY = 'ActiveDirectory';
26     const LDAP   = 'Ldap';
27     const SQL    = 'Sql';
28     const TYPO3  = 'Typo3';
29     
30     /**
31      * user status constants
32      * 
33      * @var string
34      * 
35      * @todo use constants from model
36      */
37     const STATUS_BLOCKED  = 'blocked';
38     const STATUS_DISABLED = 'disabled';
39     const STATUS_ENABLED  = 'enabled';
40     const STATUS_EXPIRED  = 'expired';
41     
42     /**
43      * Key under which the default user group name setting will be stored/retrieved
44      *
45      */
46     const DEFAULT_USER_GROUP_NAME_KEY = 'defaultUserGroupName';
47     
48     /**
49      * Key under which the default admin group name setting will be stored/retrieved
50      *
51      */
52     const DEFAULT_ADMIN_GROUP_NAME_KEY = 'defaultAdminGroupName';
53     
54     protected static $_contact2UserMapping = array(
55         'n_family'      => 'accountLastName',
56         'n_given'       => 'accountFirstName',
57         'n_fn'          => 'accountFullName',
58         'n_fileas'      => 'accountDisplayName',
59         'email'         => 'accountEmailAddress',
60         'container_id'  => 'container_id',
61     );
62     
63     /**
64      * the constructor
65      *
66      * don't use the constructor. use the singleton 
67      */
68     private function __construct() {}
69     
70     /**
71      * don't clone. Use the singleton.
72      */
73     private function __clone() {}
74
75     /**
76      * holds the instance of the singleton
77      *
78      * @var Tinebase_User_Interface
79      */
80     private static $_instance = NULL;
81
82     /**
83      * Holds the accounts backend type (e.g. Ldap or Sql.
84      * Property is lazy loaded on first access via getter {@see getConfiguredBackend()}
85      * 
86      * @var array | optional
87      */
88     private static $_backendType;
89     
90     /**
91      * Holds the backend configuration options.
92      * Property is lazy loaded from {@see Tinebase_Config} on first access via
93      * getter {@see getBackendConfiguration()}
94      * 
95      * @var array | optional
96      */
97     private static $_backendConfiguration;
98     
99     /**
100      * Holds the backend configuration options.
101      * Property is lazy loaded from {@see Tinebase_Config} on first access via
102      * getter {@see getBackendConfiguration()}
103      * 
104      * @var array | optional
105      */
106     private static $_backendConfigurationDefaults = array(
107         self::SQL => array(
108             self::DEFAULT_USER_GROUP_NAME_KEY  => Tinebase_Group::DEFAULT_USER_GROUP,
109             self::DEFAULT_ADMIN_GROUP_NAME_KEY => Tinebase_Group::DEFAULT_ADMIN_GROUP,
110         ),
111         self::LDAP => array(
112             'host' => 'localhost',
113             'username' => '',
114             'password' => '',
115             'bindRequiresDn' => true,
116             'useStartTls' => false,
117             'useRfc2307bis' => false,
118             'userDn' => '',
119             'userFilter' => 'objectclass=posixaccount',
120             'userSearchScope' => Zend_Ldap::SEARCH_SCOPE_SUB,
121             'groupsDn' => '',
122             'groupFilter' => 'objectclass=posixgroup',
123             'groupSearchScope' => Zend_Ldap::SEARCH_SCOPE_SUB,
124             'pwEncType' => 'SSHA',
125             'minUserId' => '10000',
126             'maxUserId' => '29999',
127             'minGroupId' => '11000',
128             'maxGroupId' => '11099',
129             'groupUUIDAttribute' => 'entryUUID',
130             'userUUIDAttribute' => 'entryUUID',
131             self::DEFAULT_USER_GROUP_NAME_KEY  => Tinebase_Group::DEFAULT_USER_GROUP,
132             self::DEFAULT_ADMIN_GROUP_NAME_KEY => Tinebase_Group::DEFAULT_ADMIN_GROUP,
133             'readonly' => false,
134         ),
135         self::ACTIVEDIRECTORY => array(
136             'host' => 'localhost',
137             'username' => '',
138             'password' => '',
139             'bindRequiresDn' => true,
140             'useRfc2307' => false,
141             'userDn' => '',
142             'userFilter' => 'objectclass=user',
143             'userSearchScope' => Zend_Ldap::SEARCH_SCOPE_SUB,
144             'groupsDn' => '',
145             'groupFilter' => 'objectclass=group',
146             'groupSearchScope' => Zend_Ldap::SEARCH_SCOPE_SUB,
147             'minUserId' => '10000',
148             'maxUserId' => '29999',
149             'minGroupId' => '11000',
150             'maxGroupId' => '11099',
151             'groupUUIDAttribute' => 'objectGUID',
152             'userUUIDAttribute' => 'objectGUID',
153             self::DEFAULT_USER_GROUP_NAME_KEY  => 'Domain Users',
154             self::DEFAULT_ADMIN_GROUP_NAME_KEY => 'Domain Admins',
155             'readonly' => false,
156          )
157     );
158     
159     /**
160      * the singleton pattern
161      *
162      * @return Tinebase_User_Abstract
163      */
164     public static function getInstance() 
165     {
166         if (self::$_instance === NULL) {
167             $backendType = self::getConfiguredBackend();
168             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' accounts backend: ' . $backendType);
169             
170             self::$_instance = self::factory($backendType);
171         }
172         
173         return self::$_instance;
174     }
175         
176     /**
177      * return an instance of the current user backend
178      *
179      * @param   string $backendType name of the user backend
180      * @return  Tinebase_User_Abstract
181      * @throws  Tinebase_Exception_InvalidArgument
182      */
183     public static function factory($backendType) 
184     {
185         $options = self::getBackendConfiguration();
186         
187         // this is a dangerous TRACE as there might be passwords in here!
188         //if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' 
189         //    . print_r($options, TRUE));
190         
191         $options['plugins'] = array();
192         
193         // manage email user settings
194         if (Tinebase_EmailUser::manages(Tinebase_Config::IMAP)) {
195             try {
196                 $options['plugins'][] = Tinebase_EmailUser::getInstance(Tinebase_Config::IMAP);
197             } catch (Exception $e) {
198                 if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__
199                     . ' Could not add IMAP EmailUser plugin: ' . $e);
200             }
201         }
202         if (Tinebase_EmailUser::manages(Tinebase_Config::SMTP)) {
203             try {
204                 $options['plugins'][] = Tinebase_EmailUser::getInstance(Tinebase_Config::SMTP);
205                         } catch (Exception $e) {
206             if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__
207                     . ' Could not add SMTP EmailUser plugin: ' . $e);
208             }
209         }
210         
211         switch ($backendType) {
212             case self::ACTIVEDIRECTORY:
213                 $result  = new Tinebase_User_ActiveDirectory($options);
214                 
215                 break;
216                 
217             case self::LDAP:
218                 // manage samba sam?
219                 if (isset(Tinebase_Core::getConfig()->samba) && Tinebase_Core::getConfig()->samba->get('manageSAM', FALSE) == true) {
220                     $options['plugins'][] = new Tinebase_User_Plugin_Samba(Tinebase_Core::getConfig()->samba->toArray());
221                 }
222                 
223                 $result  = new Tinebase_User_Ldap($options);
224                 
225                 break;
226                 
227             case self::SQL:
228                 $result = new Tinebase_User_Sql($options);
229                 
230                 break;
231             
232             case self::TYPO3:
233                 $result = new Tinebase_User_Typo3($options);
234                 
235                 break;
236                 
237             default:
238                 throw new Tinebase_Exception_InvalidArgument("User backend type $backendType not implemented.");
239         }
240         
241         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
242             . ' Created user backend of type ' . get_class($result));
243         
244         return $result;
245     }
246     
247     /**
248      * returns the configured rs backend
249      * 
250      * @return string
251      */
252     public static function getConfiguredBackend()
253     {
254         if (!isset(self::$_backendType)) {
255             if (Setup_Controller::getInstance()->isInstalled('Tinebase')) {
256                 self::setBackendType(Tinebase_Config::getInstance()->get(Tinebase_Config::USERBACKENDTYPE, self::SQL));
257             } else {
258                 self::setBackendType(self::SQL);
259             }
260         }
261         
262         return self::$_backendType;
263     }
264     
265     /**
266      * setter for {@see $_backendType}
267      * 
268      * @todo persist in db
269      * 
270      * @param string $backendType
271      * @return void
272      */
273     public static function setBackendType($backendType)
274     {
275         if (empty($backendType)) {
276             throw new Tinebase_Exception_InvalidArgument('Backend type can not be empty!');
277         }
278         
279         $newBackendType = ucfirst($backendType);
280         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
281             ' Setting backend type to ' . $newBackendType);
282         
283         self::$_backendType = $newBackendType;
284     }
285     
286     /**
287      * Setter for {@see $_backendConfiguration}
288      * 
289      * NOTE:
290      * Setting will not be written to Database or Filesystem.
291      * To persist the change call {@see saveBackendConfiguration()}
292      * 
293      * @param mixed $_value
294      * @param string $_key
295      * @param boolean $_applyDefaults
296      * @return void
297      * 
298      * @todo generalize this (see Tinebase_Auth::setBackendConfiguration)
299      */
300     public static function setBackendConfiguration($_value, $_key = null, $_applyDefaults = false)
301     {
302         $defaultValues = self::$_backendConfigurationDefaults[self::getConfiguredBackend()];
303         
304         if (is_null($_key) && !is_array($_value)) {
305             throw new Tinebase_Exception_InvalidArgument('To set backend configuration either a key and value '
306                 . 'parameter are required or the value parameter should be a hash');
307         } elseif (is_null($_key) && is_array($_value)) {
308             $configToSet = $_applyDefaults ? array_merge($defaultValues, $_value) : $_value;
309             foreach ($configToSet as $key => $value) {
310                 self::setBackendConfiguration($value, $key);
311             }
312         } else {
313             if ( ! (isset($defaultValues[$_key]) || array_key_exists($_key, $defaultValues))) {
314                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
315                     " Cannot set backend configuration option '$_key' for accounts storage " . self::getConfiguredBackend());
316                 return;
317             }
318             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
319                 ' Setting backend key ' . $_key . ' to ' . (preg_match('/password|pwd|pass|passwd/i', $_key) ? '********' : $_value));
320             
321             self::$_backendConfiguration[$_key] = $_value;
322         }
323     }
324     
325     /**
326      * Delete the given config setting or all config settings if {@param $_key} is not specified
327      * 
328      * @param string | optional $_key
329      * @return void
330      */
331     public static function deleteBackendConfiguration($_key = null)
332     {
333         if (is_null($_key)) {
334             self::$_backendConfiguration = array();
335         } elseif ((isset(self::$_backendConfiguration[$_key]) || array_key_exists($_key, self::$_backendConfiguration))) {
336             unset(self::$_backendConfiguration[$_key]);
337         } else {
338             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' configuration option does not exist: ' . $_key);
339         }
340     }
341     
342     /**
343      * Write backend configuration setting {@see $_backendConfigurationSettings} and {@see $_backendType} to
344      * db config table.
345      * 
346      * @return void
347      */
348     public static function saveBackendConfiguration()
349     {
350         Tinebase_Config::getInstance()->set(Tinebase_Config::USERBACKEND, self::getBackendConfiguration());
351         Tinebase_Config::getInstance()->set(Tinebase_Config::USERBACKENDTYPE, self::getConfiguredBackend());
352     }
353     
354     /**
355      * Getter for {@see $_backendConfiguration}
356      * 
357      * @param String | optional $_key
358      * @return mixed [If {@param $_key} is set then only the specified option is returned, otherwise the whole options hash]
359      */
360     public static function getBackendConfiguration($_key = null, $_default = null)
361     {
362         //lazy loading for $_backendConfiguration
363         if (!isset(self::$_backendConfiguration)) {
364             if (Setup_Controller::getInstance()->isInstalled('Tinebase')) {
365                 $rawBackendConfiguration = Tinebase_Config::getInstance()->get(Tinebase_Config::USERBACKEND, new Tinebase_Config_Struct())->toArray();
366             } else {
367                 $rawBackendConfiguration = array();
368             }
369             self::$_backendConfiguration = is_array($rawBackendConfiguration) ? $rawBackendConfiguration : Zend_Json::decode($rawBackendConfiguration);
370         }
371
372         if (isset($_key)) {
373             return (isset(self::$_backendConfiguration[$_key]) || array_key_exists($_key, self::$_backendConfiguration)) ? self::$_backendConfiguration[$_key] : $_default;
374         } else {
375             return self::$_backendConfiguration;
376         }
377     }
378     
379     /**
380      * Returns default configuration for all supported backends 
381      * and overrides the defaults with concrete values stored in this configuration 
382      * 
383      * @param boolean $_getConfiguredBackend
384      * @return mixed [If {@param $_key} is set then only the specified option is returned, otherwise the whole options hash]
385      */
386     public static function getBackendConfigurationWithDefaults($_getConfiguredBackend = TRUE)
387     {
388         $config = array();
389         $defaultConfig = self::getBackendConfigurationDefaults();
390         foreach ($defaultConfig as $backendType => $backendConfig) {
391             $config[$backendType] = ($_getConfiguredBackend && $backendType == self::getConfiguredBackend() ? self::getBackendConfiguration() : array());
392             if (is_array($config[$backendType])) {
393                 foreach ($backendConfig as $key => $value) {
394                     if (! (isset($config[$backendType][$key]) || array_key_exists($key, $config[$backendType]))) {
395                         $config[$backendType][$key] = $value;
396                     }
397                 }
398             } else {
399                 $config[$backendType] = $backendConfig;
400             }
401         }
402         return $config;
403     }
404     
405     /**
406      * Getter for {@see $_backendConfigurationDefaults}
407      * @param String | optional $_backendType
408      * @return array
409      */
410     public static function getBackendConfigurationDefaults($_backendType = null) {
411         if ($_backendType) {
412             if (!(isset(self::$_backendConfigurationDefaults[$_backendType]) || array_key_exists($_backendType, self::$_backendConfigurationDefaults))) {
413                 throw new Tinebase_Exception_InvalidArgument("Unknown backend type '$_backendType'");
414             }
415             return self::$_backendConfigurationDefaults[$_backendType];
416         } else {
417             return self::$_backendConfigurationDefaults;
418         }
419     }
420     
421     /**
422      * synchronize user from syncbackend to local sql backend
423      * 
424      * @param  mixed  $username  the login id of the user to synchronize
425      * @param  array $options
426      * @return Tinebase_Model_FullUser
427      * @throws Tinebase_Exception
428      * 
429      * @todo make use of dbmail plugin configurable (should be false by default)
430      * @todo switch to new primary group if it could not be found
431      */
432     public static function syncUser($username, $options = array())
433     {
434         if ($username instanceof Tinebase_Model_FullUser) {
435             $username = $username->accountLoginName;
436         }
437         
438         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . "  sync user data for: " . $username);
439         
440         $userBackend  = Tinebase_User::getInstance();
441         if (isset($options['ldapplugins']) && is_array($options['ldapplugins'])) {
442             foreach ($options['ldapplugins'] as $plugin) {
443                 $userBackend->registerLdapPlugin($plugin);
444             }
445         }
446         
447         $user = $userBackend->getUserByPropertyFromSyncBackend('accountLoginName', $username, 'Tinebase_Model_FullUser');
448         $user->accountPrimaryGroup = Tinebase_Group::getInstance()->resolveGIdNumberToUUId($user->accountPrimaryGroup);
449         
450         $userProperties = method_exists($userBackend, 'getLastUserProperties') ? $userBackend->getLastUserProperties() : array();
451         
452         $hookResult = self::_syncUserHook($user, $userProperties);
453         if (! $hookResult) {
454             return null;
455         }
456         
457         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' 
458             . print_r($user->toArray(), TRUE));
459         
460         self::getPrimaryGroupForUser($user);
461
462         try {
463             $syncedUser = self::_syncDataAndUpdateUser($user, $options);
464
465         } catch (Tinebase_Exception_NotFound $ten) {
466             try {
467                 $invalidUser = $userBackend->getUserByPropertyFromSqlBackend('accountLoginName', $username, 'Tinebase_Model_FullUser');
468                 if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ 
469                     . " Remove invalid user: " . $username);
470                 $userBackend->deleteUserInSqlBackend($invalidUser);
471             } catch (Tinebase_Exception_NotFound $ten) {
472                 // do nothing
473             }
474             
475             if ($user->visibility !== Tinebase_Model_FullUser::VISIBILITY_HIDDEN) {
476                 self::createContactForSyncedUser($user);
477             }
478             Tinebase_Timemachine_ModificationLog::setRecordMetaData($user, 'create');
479             $syncedUser = $userBackend->addUserInSqlBackend($user);
480             $userBackend->addPluginUser($syncedUser, $user);
481         }
482         
483         self::syncContactData($syncedUser, $options);
484         
485         Tinebase_Group::syncMemberships($syncedUser);
486
487         return $syncedUser;
488     }
489
490     /**
491      * sync account data and update
492      *
493      * @param Tinebase_Model_FullUser $user
494      * @param array $options
495      * @return mixed
496      * @throws Tinebase_Exception_InvalidArgument
497      */
498     protected static function _syncDataAndUpdateUser($user, $options)
499     {
500         $currentUser = Tinebase_User::getInstance()->getUserByProperty('accountId', $user, 'Tinebase_Model_FullUser');
501
502         $fieldsToSync = array('accountLoginName', 'accountLastPasswordChange', 'accountExpires', 'accountPrimaryGroup',
503             'accountDisplayName', 'accountLastName', 'accountFirstName', 'accountFullName', 'accountEmailAddress',
504             'accountHomeDirectory', 'accountLoginShell');
505         if (isset($options['syncAccountStatus']) && $options['syncAccountStatus']) {
506             $fieldsToSync[] = 'accountStatus';
507         }
508         $recordNeedsUpdate = false;
509         foreach ($fieldsToSync as $field) {
510             if ($currentUser->{$field} !== $user->{$field}) {
511                 $currentUser->{$field} = $user->{$field};
512                 $recordNeedsUpdate = true;
513             }
514         }
515
516         if (! empty($user->visibility) && $currentUser->visibility !== $user->visibility) {
517             $currentUser->visibility            = $user->visibility;
518             if (empty($currentUser->contact_id) && $currentUser->visibility == Tinebase_Model_FullUser::VISIBILITY_DISPLAYED) {
519                 self::createContactForSyncedUser($currentUser);
520             }
521         }
522
523         if ($recordNeedsUpdate) {
524             Tinebase_Timemachine_ModificationLog::setRecordMetaData($currentUser, 'update');
525             $syncedUser = Tinebase_User::getInstance()->updateUserInSqlBackend($currentUser);
526         } else {
527             $syncedUser = $currentUser;
528         }
529         if (! empty($user->container_id)) {
530             $syncedUser->container_id = $user->container_id;
531         }
532         Tinebase_User::getInstance()->updatePluginUser($syncedUser, $user);
533
534         return $syncedUser;
535     }
536
537     /**
538      * import contact data(phone, address, fax, birthday. photo)
539      * 
540      * @param Tinebase_Model_FullUser $syncedUser
541      * @param array $options
542      */
543     public static function syncContactData($syncedUser, $options)
544     {
545         if (! Tinebase_Config::getInstance()->get(Tinebase_Config::SYNC_USER_CONTACT_DATA, true)
546                 || ! isset($options['syncContactData'])
547                 || ! $options['syncContactData']
548                 || ! Tinebase_Application::getInstance()->isInstalled('Addressbook')
549                 ||   $syncedUser->visibility === Tinebase_Model_FullUser::VISIBILITY_HIDDEN
550         ) {
551             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
552                 . ' Contact data sync disabled');
553             return;
554         }
555         
556         $addressbook = Addressbook_Backend_Factory::factory(Addressbook_Backend_Factory::SQL);
557
558         try {
559             $contact = $addressbook->getByUserId($syncedUser->getId());
560             $originalContact = clone $contact;
561
562             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
563                 . ' user: ' .print_r($syncedUser->toArray(), true));
564
565             Tinebase_User::getInstance()->updateContactFromSyncBackend($syncedUser, $contact);
566             $contact = self::_user2Contact($syncedUser, $contact);
567
568             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
569                 . ' new contact: ' . print_r($contact->toArray(), true)
570                 . ' orig contact:' . print_r($originalContact->toArray(), true));
571
572             $syncPhoto = isset($options['syncContactPhoto']) && $options['syncContactPhoto'];
573             $diff = $contact->diff($originalContact, $syncPhoto ? array() : array('jpegphoto'));
574             if (! $diff->isEmpty() || ($originalContact->jpegphoto == 0 && ! empty($contact->jpegphoto))) {
575                 // add modlog info
576                 Tinebase_Timemachine_ModificationLog::setRecordMetaData($contact, 'update');
577                 if ($contact->container_id !== null) {
578                     Tinebase_Container::getInstance()->increaseContentSequence($contact->container_id);
579                 }
580                 
581                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
582                     . ' Updating contact data for user ' . $syncedUser->accountLoginName);
583                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
584                     . ' Diff: ' . print_r($diff->toArray(), true));
585
586                 $addressbook->update($contact);
587             } else {
588                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
589                     . ' User contact is up to date.');
590             }
591         } catch (Addressbook_Exception_NotFound $aenf) {
592             self::createContactForSyncedUser($syncedUser);
593             $syncedUser = Tinebase_User::getInstance()->updateUserInSqlBackend($syncedUser);
594
595         } catch (Tinebase_Exception_NotFound $tenf) {
596             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
597                 . ' Contact information seems to be missing in sync backend');
598             Tinebase_Exception::log($tenf);
599         }
600     }
601     
602     /**
603      * get primary group for user and make sure that group exists
604      * 
605      * @param Tinebase_Model_FullUser $user
606      * @throws Tinebase_Exception
607      * @return Tinebase_Model_Group
608      */
609     public static function getPrimaryGroupForUser($user)
610     {
611         $groupBackend = Tinebase_Group::getInstance();
612         
613         try {
614             $group = $groupBackend->getGroupById($user->accountPrimaryGroup);
615         } catch (Tinebase_Exception_Record_NotDefined $tern) {
616             if ($groupBackend->isDisabledBackend()) {
617                 // groups are sql only
618                 $group = $groupBackend->getDefaultGroup();
619                 $user->accountPrimaryGroup = $group->getId();
620             } else {
621                 try {
622                     $group = $groupBackend->getGroupByIdFromSyncBackend($user->accountPrimaryGroup);
623                 } catch (Tinebase_Exception_Record_NotDefined $ternd) {
624                     throw new Tinebase_Exception('Primary group ' . $user->accountPrimaryGroup . ' not found in sync backend.');
625                 }
626                 try {
627                     $groupBackend->getGroupByName($group->name);
628                     throw new Tinebase_Exception('Group already exists but it has a different ID: ' . $group->name);
629         
630                 } catch (Tinebase_Exception_Record_NotDefined $tern) {
631                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
632                         . " Adding group " . $group->name);
633                     $group = $groupBackend->addGroupInSqlBackend($group);
634                 }
635             }
636         }
637         
638         return $group;
639     }
640     
641     /**
642      * call configured hooks for adjusting synced user data
643      * 
644      * @param Tinebase_Model_FullUser $user
645      * @param array $userProperties
646      * @return boolean if false, user is skipped
647      */
648     protected static function _syncUserHook(Tinebase_Model_FullUser $user, $userProperties)
649     {
650         $result = true;
651         $hookClass = Tinebase_Config::getInstance()->get(Tinebase_Config::SYNC_USER_HOOK_CLASS);
652         if ($hookClass && class_exists($hookClass)) {
653             $hook = new $hookClass();
654             if (method_exists($hook, 'syncUser')) {
655                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
656                     . ' Calling ' . $hookClass . '::syncUser() ...');
657                 
658                 try {
659                     $result = call_user_func_array(array($hook, 'syncUser'), array($user, $userProperties));
660                 } catch (Tinebase_Exception $te) {
661                     Tinebase_Exception::log($te);
662                     return false;
663                 }
664             }
665         }
666         
667         return $result;
668     }
669     
670     /**
671      * create contact in addressbook
672      * 
673      * @param Tinebase_Model_FullUser $user
674      */
675     public static function createContactForSyncedUser($user)
676     {
677         if (! Tinebase_Application::getInstance()->isInstalled('Addressbook')) {
678             return;
679         }
680         
681         $contact = self::_user2Contact($user);
682         
683         // add modlog info
684         Tinebase_Timemachine_ModificationLog::setRecordMetaData($contact, 'create');
685         
686         $addressbook = Addressbook_Backend_Factory::factory(Addressbook_Backend_Factory::SQL);
687         $contact = $addressbook->create($contact);
688         
689         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
690             . " Added contact " . $contact->n_given);
691         
692         $user->contact_id = $contact->getId();
693     }
694     
695     /**
696      * sync user data to contact
697      * 
698      * @param Tinebase_Model_FullUser $user
699      * @param Addressbook_Model_Contact $contact
700      * @return Addressbook_Model_Contact
701      */
702     protected static function _user2Contact($user, $contact = null)
703     {
704         if ($contact === null) {
705             $contact = new Addressbook_Model_Contact(array(), true);
706         }
707         
708         $contact->type = Addressbook_Model_Contact::CONTACTTYPE_USER;
709         
710         foreach (self::$_contact2UserMapping as $contactKey => $userKey) {
711             if (! empty($contact->{$contactKey}) && $contact->{$contactKey} == $user->{$userKey}) {
712                 continue;
713             }
714             
715             switch ($contactKey) {
716                 case 'container_id':
717                     $contact->container_id = (! empty($user->container_id)) ? $user->container_id : Admin_Controller_User::getInstance()->getDefaultInternalAddressbook();
718                     break;
719                 default:
720                     $contact->{$contactKey} = $user->{$userKey};
721             }
722         }
723         
724         return $contact;
725     }
726     
727     /**
728      * import users from sync backend
729      * 
730      * @param array $options
731      */
732     public static function syncUsers($options)
733     {
734         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
735             .' Start synchronizing users with options ' . print_r($options, true));
736         
737         $users = Tinebase_User::getInstance()->getUsersFromSyncBackend(NULL, NULL, 'ASC', NULL, NULL, 'Tinebase_Model_FullUser');
738         
739         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
740             . ' About to sync ' . count($users) . ' users from sync backend ...');
741         
742         foreach ($users as $user) {
743             try {
744                 self::syncUser($user, $options);
745             } catch (Tinebase_Exception_NotFound $ten) {
746                 Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . " User {$user->accountLoginName} not synced: "
747                     . $ten->getMessage());
748             }
749         }
750
751         if (isset($options['deleteUsers']) && $options['deleteUsers']) {
752             self::_syncDeletedUsers($users);
753         }
754         
755         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
756             . ' Finished synchronizing users.');
757     }
758
759     /**
760      * deletes user in tine20 db that no longer exist in sync backend
761      *
762      * @param Tinebase_Record_RecordSet $usersInSyncBackend
763      */
764     protected static function _syncDeletedUsers(Tinebase_Record_RecordSet $usersInSyncBackend)
765     {
766         $userIdsInSqlBackend = Tinebase_User::getInstance()->getAllUserIdsFromSqlBackend();
767         $deletedInSyncBackend = array_diff($userIdsInSqlBackend, $usersInSyncBackend->getArrayOfIds());
768
769         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
770             . ' About to delete / expire ' . count($deletedInSyncBackend) . ' users in SQL backend...');
771
772         foreach ($deletedInSyncBackend as $userToDelete) {
773             $user = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $userToDelete, 'Tinebase_Model_FullUser');
774
775             if (in_array($user->accountLoginName, self::getSystemUsernames())) {
776                 return;
777             }
778
779             // at first, we expire+deactivate the user
780             $now = Tinebase_DateTime::now();
781             if (! $user->accountExpires || $user->accountStatus !== Tinebase_Model_User::ACCOUNT_STATUS_DISABLED) {
782                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
783                     . ' Disable user and set expiry date of ' . $user->accountLoginName . ' to ' . $now);
784                 $user->accountExpires = $now;
785                 $user->accountStatus = Tinebase_Model_User::ACCOUNT_STATUS_DISABLED;
786                 Tinebase_User::getInstance()->updateUserInSqlBackend($user);
787             } else {
788                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
789                     . ' User already expired ' . print_r($user->toArray(), true));
790
791                 // TODO make time span configurable?
792                 if ($user->accountExpires->isEarlier($now->subYear(1))) {
793                     // if he or she is already expired longer than configured expiry, we remove them!
794                     Tinebase_User::getInstance()->deleteUserInSqlBackend($userToDelete);
795
796                     if (Tinebase_Application::getInstance()->isInstalled('Addressbook') === true && ! empty($user->contact_id)) {
797                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
798                             . ' Deleting user contact of ' . $user->accountLoginName);
799
800                         $contactsBackend = Addressbook_Backend_Factory::factory(Addressbook_Backend_Factory::SQL);
801                         $contactsBackend->delete($user->contact_id);
802                     }
803                 } else {
804                     // keep user in expiry state
805                 }
806             }
807         }
808     }
809
810     /**
811      * returns login_names of system users
812      *
813      * @return array
814      */
815     public static function getSystemUsernames()
816     {
817         return array('cronuser', 'calendarscheduling');
818     }
819
820     /**
821      * get all user passwords from ldap
822      * - set pw for user (in sql and sql plugins)
823      * - do not encrypt the pw again as it is encrypted in LDAP
824      * 
825      * @throws Tinebase_Exception_Backend
826      */
827     public static function syncLdapPasswords()
828     {
829         $userBackend = Tinebase_User::getInstance();
830         if (! $userBackend instanceof Tinebase_User_Ldap) {
831             throw new Tinebase_Exception_Backend('Needs LDAP accounts backend');
832         }
833         
834         $result = $userBackend->getUserAttributes(array('entryUUID', 'userPassword'));
835         
836         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
837             . ' About to sync ' . count($result) . ' user passwords from LDAP to Tine 2.0.');
838         
839         $sqlBackend = Tinebase_User::factory(self::SQL);
840         foreach ($result as $user) {
841             try {
842                 $sqlBackend->setPassword($user['entryUUID'], $user['userPassword'], FALSE);
843             } catch (Tinebase_Exception_NotFound $tenf) {
844                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
845                     . ' Could not find user with id ' . $user['entryUUID'] . ' in SQL backend.');
846             }
847         }
848     }
849     
850     /**
851      * create initial admin account
852      * 
853      * Method is called during Setup Initialization
854      *
855      * $_options may contain the following keys:
856      * <code>
857      * $options = array(
858      *  'adminLoginName'    => 'admin',
859      *  'adminPassword'     => 'lars',
860      *  'adminFirstName'    => 'Tine 2.0',
861      *  'adminLastName'     => 'Admin Account',
862      *  'adminEmailAddress' => 'admin@tine20domain.org',
863      *  'expires'            => Tinebase_DateTime object
864      * );
865      * </code>
866      *
867      * @param array $_options [hash that may contain override values for admin user name and password]
868      * @return void
869      * @throws Tinebase_Exception_InvalidArgument
870      */
871     public static function createInitialAccounts($_options)
872     {
873         if (! isset($_options['adminPassword']) || ! isset($_options['adminLoginName'])) {
874             throw new Tinebase_Exception_InvalidArgument('Admin password and login name have to be set when creating initial account.', 503);
875         }
876         
877         $adminLoginName     = $_options['adminLoginName'];
878         $adminPassword      = $_options['adminPassword'];
879         $adminFirstName     = isset($_options['adminFirstName'])    ? $_options['adminFirstName'] : 'Tine 2.0';
880         $adminLastName      = isset($_options['adminLastName'])     ? $_options['adminLastName']  : 'Admin Account';
881         $adminEmailAddress  = ((isset($_options['adminEmailAddress']) || array_key_exists('adminEmailAddress', $_options))) ? $_options['adminEmailAddress'] : NULL;
882
883         // get admin & user groups
884         $userBackend   = Tinebase_User::getInstance();
885         $groupsBackend = Tinebase_Group::getInstance();
886         
887         $adminGroup = $groupsBackend->getDefaultAdminGroup();
888         $userGroup  = $groupsBackend->getDefaultGroup();
889         
890         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Creating initial admin user (login: ' . $adminLoginName . ' / email: ' . $adminEmailAddress . ')');
891
892         $user = new Tinebase_Model_FullUser(array(
893             'accountLoginName'      => $adminLoginName,
894             'accountStatus'         => Tinebase_Model_User::ACCOUNT_STATUS_ENABLED,
895             'accountPrimaryGroup'   => $userGroup->getId(),
896             'accountLastName'       => $adminLastName,
897             'accountDisplayName'    => $adminLastName . ', ' . $adminFirstName,
898             'accountFirstName'      => $adminFirstName,
899             'accountExpires'        => (isset($_options['expires'])) ? $_options['expires'] : NULL,
900             'accountEmailAddress'   => $adminEmailAddress
901         ));
902         
903         if ($adminEmailAddress !== NULL) {
904             $user->imapUser = new Tinebase_Model_EmailUser(array(
905                 'emailPassword' => $adminPassword
906             ));
907             $user->smtpUser = new Tinebase_Model_EmailUser(array(
908                 'emailPassword' => $adminPassword
909             ));
910         }
911
912         // update or create user in local sql backend
913         try {
914             $userBackend->getUserByProperty('accountLoginName', $adminLoginName);
915             Tinebase_Timemachine_ModificationLog::setRecordMetaData($user, 'update');
916             $user = $userBackend->updateUserInSqlBackend($user);
917         } catch (Tinebase_Exception_NotFound $ten) {
918             // call addUser here to make sure, sql user plugins (email, ...) are triggered
919             Tinebase_Timemachine_ModificationLog::setRecordMetaData($user, 'create');
920             $user = $userBackend->addUser($user);
921         }
922         
923         // set the password for the account
924         // empty password triggers password change dialogue during first login
925         if (!empty($adminPassword)) {
926             Tinebase_User::getInstance()->setPassword($user, $adminPassword);
927         }
928
929         // add the admin account to all groups
930         Tinebase_Group::getInstance()->addGroupMember($adminGroup, $user);
931         Tinebase_Group::getInstance()->addGroupMember($userGroup, $user);
932     }
933 }