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