show failing user plugin class name
[tine20] / tine20 / Tinebase / User / Sql.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-2017 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  * 
11  * @todo        extend Tinebase_Application_Backend_Sql and replace some functions
12  */
13
14 /**
15  * sql implementation of the SQL users interface
16  * 
17  * @package     Tinebase
18  * @subpackage  User
19  */
20 class Tinebase_User_Sql extends Tinebase_User_Abstract
21 {
22     use Tinebase_Controller_Record_ModlogTrait;
23
24     /**
25      * Model name
26      *
27      * @var string
28      *
29      * @todo perhaps we can remove that and build model name from name of the class (replace 'Controller' with 'Model')
30      */
31     protected $_modelName = 'Tinebase_Model_FullUser';
32
33     /**
34      * row name mapping 
35      * 
36      * @var array
37      */
38     protected $rowNameMapping = array(
39         'accountId'                 => 'id',
40         'accountDisplayName'        => 'display_name',
41         'accountFullName'           => 'full_name',
42         'accountFirstName'          => 'first_name',
43         'accountLastName'           => 'last_name',
44         'accountLoginName'          => 'login_name',
45         'accountLastLogin'          => 'last_login',
46         'accountLastLoginfrom'      => 'last_login_from',
47         'accountLastPasswordChange' => 'last_password_change',
48         'accountStatus'             => 'status',
49         'accountExpires'            => 'expires_at',
50         'accountPrimaryGroup'       => 'primary_group_id',
51         'accountEmailAddress'       => 'email',
52         'accountHomeDirectory'      => 'home_dir',
53         'accountLoginShell'         => 'login_shell',
54         'lastLoginFailure'          => 'last_login_failure_at',
55         'loginFailures'             => 'login_failures',
56         'openid'                    => 'openid',
57         'visibility'                => 'visibility',
58         'contactId'                 => 'contact_id'
59     );
60     
61     /**
62      * copy of Tinebase_Core::get('dbAdapter')
63      *
64      * @var Zend_Db_Adapter_Abstract
65      */
66     protected $_db;
67     
68     /**
69      * sql user plugins
70      * 
71      * @var array
72      */
73     protected $_sqlPlugins = array();
74     
75     /**
76      * Table name without prefix
77      *
78      * @var string
79      */
80     protected $_tableName = 'accounts';
81     
82     /**
83      * @var Tinebase_Backend_Sql_Command_Interface
84      */
85     protected $_dbCommand;
86
87     /**
88      * the constructor
89      *
90      * @param  array $options Options used in connecting, binding, etc.
91      */
92     public function __construct(array $_options = array())
93     {
94         parent::__construct($_options);
95
96         $this->_db = Tinebase_Core::getDb();
97         $this->_dbCommand = Tinebase_Backend_Sql_Command::factory($this->_db);
98     }
99
100     /**
101      * registerPlugin
102      * 
103      * @param Tinebase_User_Plugin_Interface $plugin
104      */
105     public function registerPlugin(Tinebase_User_Plugin_Interface $plugin)
106     {
107         parent::registerPlugin($plugin);
108
109         if ($plugin instanceof Tinebase_User_Plugin_SqlInterface) {
110             $className = get_class($plugin);
111
112             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
113                 . " Registering " . $className . ' SQL plugin.');
114
115             $this->_sqlPlugins[$className] = $plugin;
116         }
117     }
118
119     public function removePlugin($plugin)
120     {
121         $result = parent::removePlugin($plugin);
122
123         if ($plugin instanceof Tinebase_User_Plugin_SqlInterface) {
124             $className = get_class($plugin);
125             if (isset($this->_sqlPlugins[$className])) {
126
127                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
128                     . " Removing " . $className . ' SQL plugin.');
129
130                 $result = $this->_sqlPlugins[$className];
131                 unset($this->_sqlPlugins[$className]);
132             }
133         }
134
135         return $result;
136     }
137
138     /**
139      * @param $classname
140      * @return Tinebase_User_Plugin_SqlInterface
141      */
142     public function getSqlPlugin($classname)
143     {
144         return $this->_sqlPlugins[$classname];
145     }
146     
147     /**
148      * unregisterAllPlugins
149      */
150     public function unregisterAllPlugins()
151     {
152         parent::unregisterAllPlugins();
153         $this->_sqlPlugins = array();
154     }
155     
156     /**
157      * get list of users
158      *
159      * @param string $_filter
160      * @param string $_sort
161      * @param string $_dir
162      * @param int $_start
163      * @param int $_limit
164      * @param string $_accountClass the type of subclass for the Tinebase_Record_RecordSet to return
165      * @return Tinebase_Record_RecordSet with record class Tinebase_Model_User
166      */
167     public function getUsers($_filter = NULL, $_sort = NULL, $_dir = 'ASC', $_start = NULL, $_limit = NULL, $_accountClass = 'Tinebase_Model_User')
168     {
169         $select = $this->_getUserSelectObject()
170             ->limit($_limit, $_start);
171             
172         if ($_sort !== NULL && isset($this->rowNameMapping[$_sort])) {
173             $select->order($this->_db->table_prefix . $this->_tableName . '.' . $this->rowNameMapping[$_sort] . ' ' . $_dir);
174         }
175         
176         if (!empty($_filter)) {
177             $whereStatement = array();
178             $defaultValues  = array(
179                 $this->rowNameMapping['accountLastName'], 
180                 $this->rowNameMapping['accountFirstName'], 
181                 $this->rowNameMapping['accountLoginName']
182             );
183
184             // prepare for case insensitive search
185             foreach ($defaultValues as $defaultValue) {
186                 $whereStatement[] = $this->_dbCommand->prepareForILike($this->_db->quoteIdentifier($defaultValue)) . ' LIKE ' . $this->_dbCommand->prepareForILike('?');
187             }
188             
189             $select->where('(' . implode(' OR ', $whereStatement) . ')', '%' . $_filter . '%');
190         }
191         
192         // @todo still needed?? either we use contacts from addressboook or full users now
193         // return only active users, when searching for simple users
194         if ($_accountClass == 'Tinebase_Model_User') {
195             $select->where($this->_db->quoteInto($this->_db->quoteIdentifier('status') . ' = ?', 'enabled'));
196         }
197
198         $select->where($this->_db->quoteIdentifier($this->_db->table_prefix . $this->_tableName . '.' . 'is_deleted') . ' = 0');
199
200         $stmt = $select->query();
201         $rows = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
202         
203         $result = new Tinebase_Record_RecordSet($_accountClass, $rows, TRUE);
204         
205         return $result;
206     }
207     
208     /**
209      * get total count of users
210      *
211      * @param string $_filter
212      * @return int
213      */
214     public function getUsersCount($_filter = null)
215     {
216         $select = $this->_db->select()
217             ->from(SQL_TABLE_PREFIX . 'accounts', array('count' => 'COUNT(' . $this->_db->quoteIdentifier('id') . ')'));
218         
219         if (!empty($_filter)) {
220             $whereStatement = array();
221             $defaultValues  = array(
222                 $this->rowNameMapping['accountLastName'], 
223                 $this->rowNameMapping['accountFirstName'], 
224                 $this->rowNameMapping['accountLoginName']
225             );
226             
227             // prepare for case insensitive search
228             foreach ($defaultValues as $defaultValue) {
229                 $whereStatement[] = $this->_dbCommand->prepareForILike($this->_db->quoteIdentifier($defaultValue)) . ' LIKE ' . $this->_dbCommand->prepareForILike('?');
230             }
231             
232             $select->where('(' . implode(' OR ', $whereStatement) . ')', '%' . $_filter . '%');
233         }
234
235         $select->where($this->_db->table_prefix . $this->_tableName . '.' . $this->_db->quoteIdentifier('is_deleted') . ' = 0');
236
237         $stmt = $select->query();
238         $rows = $stmt->fetchAll(Zend_Db::FETCH_COLUMN);
239         
240         return $rows[0];
241     }
242     
243     /**
244      * get user by property
245      *
246      * @param   string  $_property      the key to filter
247      * @param   string  $_value         the value to search for
248      * @param   string  $_accountClass  type of model to return
249      * 
250      * @return  Tinebase_Model_User the user object
251      */
252     public function getUserByProperty($_property, $_value, $_accountClass = 'Tinebase_Model_User')
253     {
254         $user = $this->getUserByPropertyFromSqlBackend($_property, $_value, $_accountClass);
255         
256         // append data from plugins
257         foreach ($this->_sqlPlugins as $plugin) {
258             try {
259                 $plugin->inspectGetUserByProperty($user);
260             } catch (Tinebase_Exception_NotFound $tenf) {
261                 // do nothing
262             } catch (Exception $e) {
263                 if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__
264                          . ' User sql plugin "' . get_class($plugin) . '" failure');
265                 Tinebase_Exception::log($e);
266             }
267         }
268             
269         if ($this instanceof Tinebase_User_Interface_SyncAble) {
270             try {
271                 $syncUser = $this->getUserByPropertyFromSyncBackend('accountId', $user, $_accountClass);
272                 
273                 if (!empty($syncUser->emailUser)) {
274                     $user->emailUser  = $syncUser->emailUser;
275                 }
276                 if (!empty($syncUser->imapUser)) {
277                     $user->imapUser  = $syncUser->imapUser;
278                 }
279                 if (!empty($syncUser->smtpUser)) {
280                     $user->smtpUser  = $syncUser->smtpUser;
281                 }
282                 if (!empty($syncUser->sambaSAM)) {
283                     $user->sambaSAM  = $syncUser->sambaSAM;
284                 }
285             } catch (Tinebase_Exception_NotFound $tenf) {
286                 if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . ' user not found in sync backend: ' . $user->getId());
287             }
288         }
289         
290         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($user->toArray(), true));
291         
292         return $user;
293     }
294
295     /**
296      * get user by property
297      *
298      * @param   string  $_property      the key to filter
299      * @param   string  $_value         the value to search for
300      * @param   string  $_accountClass  type of model to return
301      * 
302      * @return  Tinebase_Model_User the user object
303      * @throws  Tinebase_Exception_NotFound
304      * @throws  Tinebase_Exception_Record_Validation
305      * @throws  Tinebase_Exception_InvalidArgument
306      */
307     public function getUserByPropertyFromSqlBackend($_property, $_value, $_accountClass = 'Tinebase_Model_User')
308     {
309         if(!(isset($this->rowNameMapping[$_property]) || array_key_exists($_property, $this->rowNameMapping))) {
310             throw new Tinebase_Exception_InvalidArgument("invalid property $_property requested");
311         }
312         
313         switch($_property) {
314             case 'accountId':
315                 $value = Tinebase_Model_User::convertUserIdToInt($_value);
316                 break;
317             default:
318                 $value = $_value;
319                 break;
320         }
321         
322         $select = $this->_getUserSelectObject()
323             ->where($this->_db->quoteInto($this->_db->quoteIdentifier( SQL_TABLE_PREFIX . 'accounts.' . $this->rowNameMapping[$_property]) . ' = ?', $value));
324
325         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $select);
326
327         $stmt = $select->query();
328
329         $row = $stmt->fetch(Zend_Db::FETCH_ASSOC);
330         if ($row === false) {
331             throw new Tinebase_Exception_NotFound('User with ' . $_property . ' = ' . $value . ' not found.');
332         }
333
334         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($row, true));
335
336         try {
337             $account = new $_accountClass(NULL, TRUE);
338             $account->setFromArray($row);
339         } catch (Tinebase_Exception_Record_Validation $e) {
340             $validation_errors = $account->getValidationErrors();
341             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . $e->getMessage() . "\n" .
342                 "Tinebase_Model_User::validation_errors: \n" .
343                 print_r($validation_errors,true));
344             throw ($e);
345         }
346         
347         return $account;
348     }
349     
350     /**
351      * get users by primary group
352      * 
353      * @param string $groupId
354      * @return Tinebase_Record_RecordSet of Tinebase_Model_FullUser
355      */
356     public function getUsersByPrimaryGroup($groupId)
357     {
358         $select = $this->_getUserSelectObject()
359             ->where($this->_db->quoteInto($this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.primary_group_id') . ' = ?', $groupId));
360         $stmt = $select->query();
361         $data = (array) $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
362         $result = new Tinebase_Record_RecordSet('Tinebase_Model_FullUser', $data, true);
363         return $result;
364     }
365     
366     /**
367      * get full user by id
368      *
369      * @param   int         $_accountId
370      * @return  Tinebase_Model_FullUser full user
371      */
372     public function getFullUserById($_accountId)
373     {
374         return $this->getUserById($_accountId, 'Tinebase_Model_FullUser');
375     }
376     
377     /**
378      * get user select
379      *
380      * @return Zend_Db_Select
381      *
382      * TODO get available fields from schema
383      */
384     protected function _getUserSelectObject()
385     {
386         $interval = $this->_dbCommand->getDynamicInterval(
387             'SECOND',
388             '1',
389             'CASE WHEN ' . $this->_db->quoteIdentifier($this->rowNameMapping['loginFailures'])
390             . ' > 5 THEN 60 ELSE POWER(2, ' . $this->_db->quoteIdentifier($this->rowNameMapping['loginFailures']) . ') END');
391         
392         $statusSQL = 'CASE WHEN ' . $this->_db->quoteIdentifier($this->rowNameMapping['accountStatus']) . ' = ' . $this->_db->quote('enabled') . ' THEN ('
393             . 'CASE WHEN '.$this->_dbCommand->setDate('NOW()') .' > ' . $this->_db->quoteIdentifier($this->rowNameMapping['accountExpires'])
394             . ' THEN ' . $this->_db->quote('expired')
395             . ' WHEN ( ' . $this->_db->quoteIdentifier($this->rowNameMapping['loginFailures']) . ' > 0 AND '
396             . $this->_db->quoteIdentifier($this->rowNameMapping['lastLoginFailure']) . ' + ' . $interval . ' > NOW()) THEN ' . $this->_db->quote('blocked')
397             . ' ELSE ' . $this->_db->quote('enabled') . ' END)'
398             . ' WHEN ' . $this->_db->quoteIdentifier($this->rowNameMapping['accountStatus']) . ' = ' . $this->_db->quote('expired')
399                 . ' THEN ' . $this->_db->quote('expired')
400             . ' ELSE ' . $this->_db->quote('disabled') . ' END';
401
402         $fields =  array(
403             'accountId'             => $this->rowNameMapping['accountId'],
404             'accountLoginName'      => $this->rowNameMapping['accountLoginName'],
405             'accountLastLogin'      => $this->rowNameMapping['accountLastLogin'],
406             'accountLastLoginfrom'  => $this->rowNameMapping['accountLastLoginfrom'],
407             'accountLastPasswordChange' => $this->rowNameMapping['accountLastPasswordChange'],
408             'accountStatus'         => $statusSQL,
409             'accountExpires'        => $this->rowNameMapping['accountExpires'],
410             'accountPrimaryGroup'   => $this->rowNameMapping['accountPrimaryGroup'],
411             'accountHomeDirectory'  => $this->rowNameMapping['accountHomeDirectory'],
412             'accountLoginShell'     => $this->rowNameMapping['accountLoginShell'],
413             'accountDisplayName'    => $this->rowNameMapping['accountDisplayName'],
414             'accountFullName'       => $this->rowNameMapping['accountFullName'],
415             'accountFirstName'      => $this->rowNameMapping['accountFirstName'],
416             'accountLastName'       => $this->rowNameMapping['accountLastName'],
417             'accountEmailAddress'   => $this->rowNameMapping['accountEmailAddress'],
418             'lastLoginFailure'      => $this->rowNameMapping['lastLoginFailure'],
419             'loginFailures'         => $this->rowNameMapping['loginFailures'],
420             'contact_id',
421             'openid',
422             'visibility',
423             'NOW()', // only needed for debugging
424         );
425
426         // modlog fields have been added later
427         if ($this->_userTableHasModlogFields()) {
428             $fields = array_merge($fields, array(
429                 'created_by',
430                 'creation_time',
431                 'last_modified_by',
432                 'last_modified_time',
433                 'is_deleted',
434                 'deleted_time',
435                 'deleted_by',
436                 'seq',
437             ));
438         }
439
440         $select = $this->_db->select()
441             ->from(SQL_TABLE_PREFIX . 'accounts', $fields)
442             ->joinLeft(
443                SQL_TABLE_PREFIX . 'addressbook',
444                $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.contact_id') . ' = ' 
445                 . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'addressbook.id'), 
446                 array(
447                     'container_id'            => 'container_id'
448                 )
449             );
450
451         return $select;
452     }
453
454     /**
455      * set the password for given account
456      *
457      * @param   string $_userId
458      * @param   string $_password
459      * @param   bool $_encrypt encrypt password
460      * @param   bool $_mustChange
461      * @throws Tinebase_Exception_NotFound
462      */
463     public function setPassword($_userId, $_password, $_encrypt = TRUE, $_mustChange = null)
464     {
465         $userId = $_userId instanceof Tinebase_Model_User ? $_userId->getId() : $_userId;
466         $user = $_userId instanceof Tinebase_Model_FullUser ? $_userId : $this->getFullUserById($userId);
467         $this->checkPasswordPolicy($_password, $user);
468
469         $accountData = $this->_updatePasswordProperty($userId, $_password, 'password', $_encrypt);
470         $this->_setPluginsPassword($userId, $_password, $_encrypt);
471
472         $accountData['id'] = $userId;
473         $oldPassword = new Tinebase_Model_UserPassword(array('id' => $userId), true);
474         $newPassword = new Tinebase_Model_UserPassword($accountData, true);
475         $this->_writeModLog($newPassword, $oldPassword);
476     }
477
478     /**
479      * @param        $_userId
480      * @param        $_password
481      * @param string $_property
482      * @param boolean  $_encrypt
483      * @return array $accountData
484      * @throws Tinebase_Exception_NotFound
485      */
486     protected function _updatePasswordProperty($_userId, $_password, $_property = 'password', $_encrypt = true)
487     {
488         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . $this->_tableName));
489
490         $accountData = array();
491         $accountData[$_property] = ($_encrypt) ? Hash_Password::generate('SSHA256', $_password) : $_password;
492         if ($_property === 'password') {
493             $accountData['last_password_change'] = Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG);
494         }
495
496         $where = array(
497             $accountsTable->getAdapter()->quoteInto($accountsTable->getAdapter()->quoteIdentifier('id') . ' = ?', $_userId)
498         );
499
500         $result = $accountsTable->update($accountData, $where);
501
502         if ($result != 1) {
503             throw new Tinebase_Exception_NotFound('Unable to update password! account not found in authentication backend.');
504         }
505
506         return $accountData;
507     }
508
509     /**
510      * set password in plugins
511      * 
512      * @param string $userId
513      * @param string $password
514      * @param bool   $encrypt encrypt password
515      * @throws Tinebase_Exception_Backend
516      */
517     protected function _setPluginsPassword($userId, $password, $encrypt = TRUE)
518     {
519         foreach ($this->_sqlPlugins as $plugin) {
520             try {
521                 $plugin->inspectSetPassword($userId, $password, $encrypt);
522             } catch (Exception $e) {
523                 Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Could not change plugin password: ' . $e);
524                 throw new Tinebase_Exception_Backend($e->getMessage());
525             }
526         }
527     }
528     
529     /**
530      * ensure password policy
531      * 
532      * @param string $password
533      * @param Tinebase_Model_FullUser $user
534      * @throws Tinebase_Exception_PasswordPolicyViolation
535      */
536     public function checkPasswordPolicy($password, Tinebase_Model_FullUser $user)
537     {
538         if (! Tinebase_Config::getInstance()->get(Tinebase_Config::PASSWORD_POLICY_ACTIVE, false)) {
539             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
540                 . ' No password policy enabled');
541             return;
542         }
543         
544         $failedTests = array();
545         
546         $policy = array(
547             Tinebase_Config::PASSWORD_POLICY_ONLYASCII              => '/[^\x00-\x7F]/',
548             Tinebase_Config::PASSWORD_POLICY_MIN_LENGTH             => null,
549             Tinebase_Config::PASSWORD_POLICY_MIN_WORD_CHARS         => '/[\W]*/',
550             Tinebase_Config::PASSWORD_POLICY_MIN_UPPERCASE_CHARS    => '/[^A-Z]*/',
551             Tinebase_Config::PASSWORD_POLICY_MIN_SPECIAL_CHARS      => '/[\w]*/',
552             Tinebase_Config::PASSWORD_POLICY_MIN_NUMBERS            => '/[^0-9]*/',
553             Tinebase_Config::PASSWORD_POLICY_FORBID_USERNAME        => $user->accountLoginName,
554         );
555         
556         foreach ($policy as $key => $regex) {
557             $test = $this->_testPolicy($password, $key, $regex);
558             if ($test !== true) {
559                 $failedTests[$key] = $test;
560             }
561         }
562         
563         if (! empty($failedTests)) {
564             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
565                 . ' ' . print_r($failedTests, true));
566             
567             $policyException = new Tinebase_Exception_PasswordPolicyViolation('Password failed to match the following policy requirements: ' 
568                 . implode('|', array_keys($failedTests)));
569             throw $policyException;
570         }
571     }
572     
573     /**
574      * test password policy
575      * 
576      * @param string $password
577      * @param string $configKey
578      * @param string $regex
579      * @return mixed
580      */
581     protected function _testPolicy($password, $configKey, $regex = null)
582     {
583         $result = true;
584         
585         switch ($configKey) {
586             case Tinebase_Config::PASSWORD_POLICY_ONLYASCII:
587                 if (Tinebase_Config::getInstance()->get($configKey, 0) && $regex !== null) {
588                     $nonAsciiFound = preg_match($regex, $password, $matches);
589                     
590                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
591                         __METHOD__ . '::' . __LINE__ . ' ' . print_r($matches, true));
592                     
593                     $result = ($nonAsciiFound) ? array('expected' => 0, 'got' => count($matches)) : true;
594                 }
595                 
596                 break;
597                 
598             case Tinebase_Config::PASSWORD_POLICY_FORBID_USERNAME:
599                 if (Tinebase_Config::getInstance()->get($configKey, 0)) {
600                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
601                         __METHOD__ . '::' . __LINE__ . ' Testing if password is part of username "' . $regex . '"');
602                     
603                     if (! empty($password)) {
604                         $result = ! preg_match('/' . preg_quote($password) . '/i', $regex);
605                     }
606                 }
607                 
608                 break;
609                 
610             default:
611                 // check min length restriction
612                 $minLength = Tinebase_Config::getInstance()->get($configKey, 0);
613                 if ($minLength > 0) {
614                     $reduced = ($regex) ? preg_replace($regex, '', $password) : $password;
615                     $charCount = strlen(utf8_decode($reduced));
616                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
617                         . ' Found ' . $charCount . '/' . $minLength . ' chars for ' . $configKey /*. ': ' . $reduced */);
618                     
619                     if ($charCount < $minLength) {
620                         $result = array('expected' => $minLength, 'got' => $charCount);
621                     }
622                 }
623                 
624                 break;
625         }
626         
627         return $result;
628     }
629     
630     /**
631      * set the status of the user
632      *
633      * @param mixed   $_accountId
634      * @param string  $_status
635      * @return integer
636      * @throws Tinebase_Exception_InvalidArgument
637      */
638     public function setStatus($_accountId, $_status)
639     {
640         if ($this instanceof Tinebase_User_Interface_SyncAble) {
641             $this->setStatusInSyncBackend($_accountId, $_status);
642         }
643         
644         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
645         
646         switch($_status) {
647             case Tinebase_Model_User::ACCOUNT_STATUS_ENABLED:
648                 $accountData[$this->rowNameMapping['loginFailures']]  = 0;
649                 $accountData[$this->rowNameMapping['accountExpires']] = null;
650                 $accountData['status'] = $_status;
651                 break;
652                 
653             case Tinebase_Model_User::ACCOUNT_STATUS_DISABLED:
654                 $accountData['status'] = $_status;
655                 break;
656                 
657             case Tinebase_Model_User::ACCOUNT_STATUS_EXPIRED:
658                 $expiryDate = Tinebase_DateTime::now()->subSecond(1);
659                 $accountData['expires_at'] = $expiryDate->toString();
660                 if ($this instanceof Tinebase_User_Interface_SyncAble) {
661                     $this->setExpiryDateInSyncBackend($_accountId, $expiryDate);
662                 }
663
664                 break;
665             
666             default:
667                 throw new Tinebase_Exception_InvalidArgument('$_status can be only enabled, disabled or expired');
668                 break;
669         }
670
671         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
672
673         $where = array(
674             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
675         );
676
677         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->INFO(__METHOD__ . ' ' . __LINE__
678             . ' ' . $_status . ' user with id ' . $accountId);
679
680         $result = $accountsTable->update($accountData, $where);
681
682         $oldUser = new Tinebase_Model_FullUser(array('accountId' => $accountId), true);
683         $newUser = new Tinebase_Model_FullUser(array('accountId' => $accountId, 'accountStatus' => $_status), true);
684         $this->_writeModLog($newUser, $oldUser);
685
686         return $result;
687     }
688
689     /**
690      * sets/unsets expiry date 
691      *
692      * @param     mixed      $_accountId
693      * @param     Tinebase_DateTime  $_expiryDate set to NULL to disable expirydate
694     */
695     public function setExpiryDate($_accountId, $_expiryDate)
696     {
697         if ($this instanceof Tinebase_User_Interface_SyncAble) {
698             $this->setExpiryDateInSyncBackend($_accountId, $_expiryDate);
699         }
700         
701         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
702         
703         if($_expiryDate instanceof DateTime) {
704             $accountData['expires_at'] = $_expiryDate->get(Tinebase_Record_Abstract::ISO8601LONG);
705         } else {
706             $accountData['expires_at'] = NULL;
707         }
708         
709         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
710
711         $where = array(
712             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
713         );
714         
715         $result = $accountsTable->update($accountData, $where);
716
717         $oldUser = new Tinebase_Model_FullUser(array('accountId' => $accountId), true);
718         $newUser = new Tinebase_Model_FullUser(array('accountId' => $accountId, 'accountExpires' => $accountData['expires_at']), true);
719         $this->_writeModLog($newUser, $oldUser);
720
721         return $result;
722     }
723     
724     /**
725      * set last login failure in accounts table
726      * 
727      * @param string $_loginName
728      * @return Tinebase_Model_FullUser|null user if found
729      * @see Tinebase/User/Tinebase_User_Interface::setLastLoginFailure()
730      */
731     public function setLastLoginFailure($_loginName)
732     {
733         try {
734             $user = $this->getUserByPropertyFromSqlBackend('accountLoginName', $_loginName, 'Tinebase_Model_FullUser');
735         } catch (Tinebase_Exception_NotFound $tenf) {
736             // nothing todo => is no existing user
737             return null;
738         }
739         
740         $values = array(
741             'last_login_failure_at' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
742             'login_failures'        => new Zend_Db_Expr($this->_db->quoteIdentifier('login_failures') . ' + 1')
743         );
744         
745         $where = array(
746             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId())
747         );
748         
749         $this->_db->update(SQL_TABLE_PREFIX . 'accounts', $values, $where);
750
751         return $user;
752     }
753     
754     /**
755      * update the lastlogin time of user
756      *
757      * @param int $_accountId
758      * @param string $_ipAddress
759      * @return integer
760      */
761     public function setLoginTime($_accountId, $_ipAddress) 
762     {
763         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
764         
765         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
766         
767         $accountData['last_login_from'] = $_ipAddress;
768         $accountData['last_login']      = Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG);
769         $accountData['login_failures']  = 0;
770         
771         $where = array(
772             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
773         );
774         
775         $result = $accountsTable->update($accountData, $where);
776         
777         return $result;
778     }
779     
780     /**
781      * update contact data(first name, last name, ...) of user
782      * 
783      * @param Addressbook_Model_Contact $contact
784      */
785     public function updateContact(Addressbook_Model_Contact $_contact)
786     {
787         if($this instanceof Tinebase_User_Interface_SyncAble) {
788             $this->updateContactInSyncBackend($_contact);
789         }
790         
791         return $this->updateContactInSqlBackend($_contact);
792     }
793     
794     /**
795      * update contact data(first name, last name, ...) of user in local sql storage
796      * 
797      * @param Addressbook_Model_Contact $contact
798      * @return integer
799      * @throws Exception
800      */
801     public function updateContactInSqlBackend(Addressbook_Model_Contact $_contact)
802     {
803         $contactId = $_contact->getId();
804
805         $oldUser = $this->getUserByProperty('contactId', $contactId, 'Tinebase_Model_FullUser');
806
807         $accountData = array(
808             $this->rowNameMapping['accountDisplayName']  => $_contact->n_fileas,
809             $this->rowNameMapping['accountFullName']     => $_contact->n_fn,
810             $this->rowNameMapping['accountFirstName']    => $_contact->n_given,
811             $this->rowNameMapping['accountLastName']     => $_contact->n_family,
812             $this->rowNameMapping['accountEmailAddress'] => $_contact->email
813         );
814         
815         try {
816             $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
817             
818             $where = array(
819                 $this->_db->quoteInto($this->_db->quoteIdentifier('contact_id') . ' = ?', $contactId)
820             );
821             $result = $accountsTable->update($accountData, $where);
822
823             $newUser = $this->getUserByPropertyFromSqlBackend('contactId', $contactId, 'Tinebase_Model_FullUser');
824             $this->_writeModLog($newUser, $oldUser);
825
826             return $result;
827
828         } catch (Exception $e) {
829             Tinebase_TransactionManager::getInstance()->rollBack();
830             throw($e);
831         }
832     }
833     
834     /**
835      * updates an user
836      * 
837      * this function updates an user 
838      *
839      * @param Tinebase_Model_FullUser $_user
840      * @return Tinebase_Model_FullUser
841      */
842     public function updateUser(Tinebase_Model_FullUser $_user)
843     {
844         $visibility = $_user->visibility;
845
846         if($this instanceof Tinebase_User_Interface_SyncAble) {
847             $this->updateUserInSyncBackend($_user);
848         }
849         
850         $updatedUser = $this->updateUserInSqlBackend($_user);
851         $this->updatePluginUser($updatedUser, $_user);
852
853         $contactId = $updatedUser->contact_id;
854         if (!empty($visibility) && !empty($contactId) && $visibility != $updatedUser->visibility) {
855             $updatedUser->visibility = $visibility;
856             $updatedUser = $this->updateUserInSqlBackend($updatedUser);
857             $this->updatePluginUser($updatedUser, $_user);
858         }
859
860         return $updatedUser;
861     }
862     
863     /**
864     * update data in plugins
865     *
866     * @param Tinebase_Model_FullUser $updatedUser
867     * @param Tinebase_Model_FullUser $newUserProperties
868     */
869     public function updatePluginUser($updatedUser, $newUserProperties)
870     {
871         foreach ($this->_sqlPlugins as $plugin) {
872             $plugin->inspectUpdateUser($updatedUser, $newUserProperties);
873         }
874     }
875     
876     /**
877      * updates an user
878      * 
879      * this function updates an user 
880      *
881      * @param Tinebase_Model_FullUser $_user
882      * @return Tinebase_Model_FullUser
883      * @throws 
884      */
885     public function updateUserInSqlBackend(Tinebase_Model_FullUser $_user)
886     {
887         if(! $_user->isValid()) {
888             throw new Tinebase_Exception_Record_Validation('Invalid user object. ' . print_r($_user->getValidationErrors(), TRUE));
889         }
890
891         $accountId = Tinebase_Model_User::convertUserIdToInt($_user);
892
893         $oldUser = $this->getFullUserById($accountId);
894         
895         if (empty($_user->contact_id)) {
896             $_user->visibility = 'hidden';
897             $_user->contact_id = null;
898         }
899         $accountData = $this->_recordToRawData($_user);
900         // don't update id
901         unset($accountData['id']);
902         
903         // ignore all other states (blocked)
904         unset($accountData[$this->rowNameMapping['accountStatus']]);
905         if ($_user->accountStatus === Tinebase_User::STATUS_ENABLED) {
906             $accountData[$this->rowNameMapping['accountStatus']] = $_user->accountStatus;
907             
908             if ($oldUser->accountStatus === Tinebase_User::STATUS_BLOCKED) {
909                 $accountData[$this->rowNameMapping['loginFailures']] = 0;
910             } elseif ($oldUser->accountStatus === Tinebase_User::STATUS_EXPIRED) {
911                 $accountData[$this->rowNameMapping['accountExpires']] = null;
912             }
913         } elseif ($_user->accountStatus === Tinebase_User::STATUS_DISABLED ||
914                 Tinebase_User::STATUS_EXPIRED === $_user->accountStatus) {
915             $accountData[$this->rowNameMapping['accountStatus']] = $_user->accountStatus;
916         }
917         
918         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($accountData, true));
919
920         try {
921             $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
922             
923             $where = array(
924                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
925             );
926             $accountsTable->update($accountData, $where);
927             
928         } catch (Exception $e) {
929             Tinebase_TransactionManager::getInstance()->rollBack();
930             throw($e);
931         }
932         
933         $newUser = $this->getUserById($accountId, 'Tinebase_Model_FullUser');
934
935         $this->_writeModLog($newUser, $oldUser);
936
937         return $newUser;
938     }
939     
940     /**
941      * add an user
942      * 
943      * @param   Tinebase_Model_FullUser  $_user
944      * @return  Tinebase_Model_FullUser
945      */
946     public function addUser(Tinebase_Model_FullUser $_user)
947     {
948         $visibility = $_user->visibility;
949
950         if ($this instanceof Tinebase_User_Interface_SyncAble) {
951             $userFromSyncBackend = $this->addUserToSyncBackend($_user);
952             if ($userFromSyncBackend !== NULL) {
953                 // set accountId for sql backend sql backend
954                 $_user->setId($userFromSyncBackend->getId());
955             }
956         }
957
958         $addedUser = $this->addUserInSqlBackend($_user);
959         $this->addPluginUser($addedUser, $_user);
960
961         $contactId = $addedUser->contact_id;
962         if (!empty($visibility) && !empty($contactId) && $visibility != $addedUser->visibility) {
963             $addedUser->visibility = $visibility;
964             $addedUser = $this->updateUserInSqlBackend($addedUser);
965             $this->updatePluginUser($addedUser, $_user);
966         }
967
968         return $addedUser;
969     }
970     
971     /**
972      * add data from/to plugins
973      * 
974      * @param Tinebase_Model_FullUser $addedUser
975      * @param Tinebase_Model_FullUser $newUserProperties
976      */
977     public function addPluginUser($addedUser, $newUserProperties)
978     {
979         foreach ($this->_sqlPlugins as $plugin) {
980             $plugin->inspectAddUser($addedUser, $newUserProperties);
981         }
982     }
983     
984     /**
985      * add an user
986      * 
987      * @todo fix $contactData['container_id'] = 1;
988      *
989      * @param   Tinebase_Model_FullUser  $_user
990      * @return  Tinebase_Model_FullUser
991      */
992     public function addUserInSqlBackend(Tinebase_Model_FullUser $_user)
993     {
994         $_user->isValid(TRUE);
995         
996         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
997         
998         if(!isset($_user->accountId)) {
999             $userId = $_user->generateUID();
1000             $_user->setId($userId);
1001         }
1002
1003         $contactId = $_user->contact_id;
1004         if (empty($contactId)) {
1005             $_user->visibility = Tinebase_Model_FullUser::VISIBILITY_HIDDEN;
1006             $_user->contact_id = null;
1007         }
1008         
1009         $accountData = $this->_recordToRawData($_user);
1010         // persist status for new users!
1011         $accountData['status'] = $_user->accountStatus;
1012         
1013         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Adding user to SQL backend: ' . $_user->accountLoginName);
1014         
1015         $accountsTable->insert($accountData);
1016
1017         $this->_writeModLog($_user, null);
1018
1019         return $this->getUserById($_user->getId(), 'Tinebase_Model_FullUser');
1020     }
1021     
1022     /**
1023      * converts record into raw data for adapter
1024      *
1025      * @param  Tinebase_Record_Interface $_user
1026      * @return array
1027      */
1028     protected function _recordToRawData(Tinebase_Record_Interface $_user)
1029     {
1030         $accountData = array(
1031             'id'                => $_user->accountId,
1032             'login_name'        => $_user->accountLoginName,
1033             'expires_at'        => ($_user->accountExpires instanceof DateTime ? $_user->accountExpires->get(Tinebase_Record_Abstract::ISO8601LONG) : NULL),
1034             'primary_group_id'  => $_user->accountPrimaryGroup,
1035             'home_dir'          => $_user->accountHomeDirectory,
1036             'login_shell'       => $_user->accountLoginShell,
1037             'openid'            => $_user->openid,
1038             'visibility'        => $_user->visibility,
1039             'contact_id'        => $_user->contact_id,
1040             $this->rowNameMapping['accountDisplayName']  => $_user->accountDisplayName,
1041             $this->rowNameMapping['accountFullName']     => $_user->accountFullName,
1042             $this->rowNameMapping['accountFirstName']    => $_user->accountFirstName,
1043             $this->rowNameMapping['accountLastName']     => $_user->accountLastName,
1044             $this->rowNameMapping['accountEmailAddress'] => $_user->accountEmailAddress,
1045             'created_by'            => $_user->created_by,
1046             'creation_time'         => $_user->creation_time,
1047             'last_modified_by'      => $_user->last_modified_by,
1048             'last_modified_time'    => $_user->last_modified_time,
1049             'is_deleted'            => $_user->is_deleted,
1050             'deleted_time'          => $_user->deleted_time,
1051             'deleted_by'            => $_user->deleted_by,
1052             'seq'                   => $_user->seq,
1053         );
1054         
1055         $unsetIfEmpty = array('seq', 'creation_time', 'created_by', 'last_modified_by', 'last_modified_time', 'is_deleted', 'deleted_time', 'deleted_by');
1056         foreach ($unsetIfEmpty as $property) {
1057             if (empty($accountData[$property])) {
1058                 unset($accountData[$property]);
1059             }
1060         }
1061         
1062         return $accountData;
1063     }
1064     
1065     /**
1066      * delete a user
1067      *
1068      * @param  mixed  $_userId
1069      */
1070     public function deleteUser($_userId)
1071     {
1072         $deletedUser = $this->deleteUserInSqlBackend($_userId);
1073         
1074         if($this instanceof Tinebase_User_Interface_SyncAble) {
1075             $this->deleteUserInSyncBackend($deletedUser);
1076         }
1077         
1078         // update data from plugins
1079         foreach ($this->_sqlPlugins as $plugin) {
1080             $plugin->inspectDeleteUser($deletedUser);
1081         }
1082         
1083     }
1084
1085     /**
1086      * hard delete a user from DB, fire Tinebase_Event_User_DeleteAccount event
1087      *
1088      * @param  string|Tinebase_Model_FullUser $_userId the user(id) to delete
1089      * @return Tinebase_Model_FullUser the deleted user
1090      * @throws Exception
1091      */
1092     public function directDeleteUserInSqlBackend($_userId)
1093     {
1094         if ($_userId instanceof Tinebase_Model_FullUser) {
1095             $user = $_userId;
1096         } else {
1097             $user = $this->getFullUserById($_userId);
1098         }
1099
1100         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1101             . ' Deleting user' . $user->accountLoginName);
1102
1103         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($this->_db);
1104
1105         try {
1106             $event = new Tinebase_Event_User_DeleteAccount(
1107                 Tinebase_Config::getInstance()->get(Tinebase_Config::ACCOUNT_DELETION_EVENTCONFIGURATION, new Tinebase_Config_Struct())->toArray()
1108             );
1109             $event->account = $user;
1110             Tinebase_Event::fireEvent($event);
1111
1112             $accountsTable          = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
1113             $groupMembersTable      = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'group_members'));
1114             $roleMembersTable       = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'role_accounts'));
1115
1116             $where  = array(
1117                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_id') . ' = ?', $user->getId()),
1118             );
1119             $groupMembersTable->delete($where);
1120
1121             $where  = array(
1122                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_id')   . ' = ?', $user->getId()),
1123                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_type') . ' = ?', Tinebase_Acl_Rights::ACCOUNT_TYPE_USER),
1124             );
1125             $roleMembersTable->delete($where);
1126
1127             $where  = array(
1128                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId()),
1129             );
1130             $accountsTable->delete($where);
1131
1132             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1133         } catch (Exception $e) {
1134             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' error while deleting account ' . $e->__toString());
1135             Tinebase_TransactionManager::getInstance()->rollBack();
1136             throw($e);
1137         }
1138
1139         return $user;
1140     }
1141
1142     /**
1143      * delete a user (delayed; its marked deleted, disabled, hidden and stripped from groups and roles immediately. Full delete and event are fired "async" via actionQueue)
1144      *
1145      * @param  mixed  $_userId
1146      * @return Tinebase_Model_FullUser  the delete user
1147      */
1148     public function deleteUserInSqlBackend($_userId)
1149     {
1150         if ($_userId instanceof Tinebase_Model_FullUser) {
1151             $user = $_userId;
1152         } else {
1153             $user = $this->getFullUserById($_userId);
1154         }
1155
1156         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1157             . ' Deleting user' . $user->accountLoginName);
1158
1159         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($this->_db);
1160
1161         try {
1162
1163             $accountsTable          = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
1164             $groupMembersTable      = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'group_members'));
1165             $roleMembersTable       = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'role_accounts'));
1166
1167             $where  = array(
1168                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_id') . ' = ?', $user->getId()),
1169             );
1170             $groupMembersTable->delete($where);
1171
1172             $where  = array(
1173                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_id')   . ' = ?', $user->getId()),
1174                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_type') . ' = ?', Tinebase_Acl_Rights::ACCOUNT_TYPE_USER),
1175             );
1176             $roleMembersTable->delete($where);
1177
1178             $where  = array(
1179                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId()),
1180             );
1181             $accountsTable->update(array(   'is_deleted' => 1,
1182                                             'status' => Tinebase_Model_User::ACCOUNT_STATUS_DISABLED,
1183                                             'visibility' => Tinebase_Model_User::VISIBILITY_HIDDEN), $where);
1184
1185             Tinebase_ActionQueue::getInstance()->queueAction('Tinebase_FOO_User.directDeleteUserInSqlBackend', $user->getId());
1186
1187             $this->_writeModLog(null, $user);
1188
1189             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1190         } catch (Exception $e) {
1191             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' error while deleting account ' . $e->__toString());
1192             Tinebase_TransactionManager::getInstance()->rollBack();
1193             throw($e);
1194         }
1195
1196         return $user;
1197     }
1198
1199     /**
1200      * delete users
1201      * 
1202      * @param array $_accountIds
1203      */
1204     public function deleteUsers(array $_accountIds)
1205     {
1206         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1207             . ' Deleting ' . count($_accountIds) .' users');
1208
1209         foreach ($_accountIds as $accountId) {
1210             $this->deleteUser($accountId);
1211         }
1212     }
1213     
1214     /**
1215      * Delete all users returned by {@see getUsers()} using {@see deleteUsers()}
1216      * 
1217      * @return void
1218      */
1219     public function deleteAllUsers()
1220     {
1221         // need to fetch FullUser because otherwise we would get only enabled accounts :/
1222         $users = $this->getUsers(NULL, NULL, 'ASC', NULL, NULL, 'Tinebase_Model_FullUser');
1223         
1224         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Deleting ' . count($users) .' users');
1225         foreach ( $users as $user ) {
1226             $this->deleteUser($user);
1227         }
1228     }
1229
1230     /**
1231      * Get multiple users
1232      *
1233      * fetch FullUser by default
1234      *
1235      * @param  string|array $_id Ids
1236      * @param  string  $_accountClass  type of model to return
1237      * @return Tinebase_Record_RecordSet of 'Tinebase_Model_User' or 'Tinebase_Model_FullUser'
1238      */
1239     public function getMultiple($_id, $_accountClass = 'Tinebase_Model_FullUser') 
1240     {
1241         if (empty($_id)) {
1242             return new Tinebase_Record_RecordSet($_accountClass);
1243         }
1244         
1245         $select = $this->_getUserSelectObject()
1246             ->where($this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.id') . ' in (?)', (array) $_id);
1247         
1248         $stmt = $this->_db->query($select);
1249         $queryResult = $stmt->fetchAll();
1250         
1251         $result = new Tinebase_Record_RecordSet($_accountClass, $queryResult, TRUE);
1252         
1253         return $result;
1254     }
1255
1256     /**
1257      * send deactivation email to user
1258      * 
1259      * @param mixed $accountId
1260      */
1261     public function sendDeactivationNotification($accountId)
1262     {
1263         if (! Tinebase_Config::getInstance()->get(Tinebase_Config::ACCOUNT_DEACTIVATION_NOTIFICATION)) {
1264             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
1265                 __METHOD__ . '::' . __LINE__ . ' Deactivation notification disabled.');
1266             return;
1267         }
1268         
1269         try {
1270             $user = $this->getFullUserById($accountId);
1271             
1272             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
1273                 __METHOD__ . '::' . __LINE__ . ' Send deactivation notification to user ' . $user->accountLoginName);
1274             
1275             $translate = Tinebase_Translation::getTranslation('Tinebase');
1276             
1277             $view = new Zend_View();
1278             $view->setScriptPath(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'views');
1279             
1280             $view->translate            = $translate;
1281             $view->accountLoginName     = $user->accountLoginName;
1282             // TODO add this?
1283             //$view->deactivationDate     = $user->deactivationDate;
1284             $view->tine20Url            = Tinebase_Core::getHostname();
1285             
1286             $messageBody = $view->render('deactivationNotification.php');
1287             $messageSubject = $translate->_('Your Tine 2.0 account has been deactivated');
1288             
1289             $recipient = Addressbook_Controller_Contact::getInstance()->getContactByUserId($user->getId(), /* $_ignoreACL = */ true);
1290             Tinebase_Notification::getInstance()->send(/* sender = */ null, array($recipient), $messageSubject, $messageBody);
1291             
1292         } catch (Exception $e) {
1293             Tinebase_Exception::log($e);
1294         }
1295     }
1296
1297     /**
1298      * returns number of current not-disabled, non-system users
1299      *
1300      * @return number
1301      */
1302     public function countNonSystemUsers()
1303     {
1304         $systemUsers = Tinebase_User::getSystemUsernames();
1305         $select = $select = $this->_db->select()
1306             ->from(SQL_TABLE_PREFIX . 'accounts', 'COUNT(id)')
1307             ->where($this->_db->quoteIdentifier('login_name') . ' not in (?)', $systemUsers)
1308             ->where($this->_db->quoteInto($this->_db->quoteIdentifier('status') . ' != ?', Tinebase_Model_User::ACCOUNT_STATUS_DISABLED))
1309             ->where($this->_db->quoteIdentifier('is_deleted') . ' = 0');
1310
1311         $userCount = $this->_db->fetchOne($select);
1312
1313         return $userCount;
1314     }
1315
1316     /**
1317      * fetch creation time of the first/oldest user
1318      *
1319      * @return Tinebase_DateTime
1320      */
1321     public function getFirstUserCreationTime()
1322     {
1323         $fallback = new Tinebase_DateTime('2014-12-01');
1324         if (! $this->_userTableHasModlogFields()) {
1325             return $fallback;
1326         }
1327
1328         $systemUsers = Tinebase_User::getSystemUsernames();
1329         $select = $select = $this->_db->select()
1330             ->from(SQL_TABLE_PREFIX . 'accounts', 'creation_time')
1331             ->where($this->_db->quoteIdentifier('login_name') . ' not in (?)', $systemUsers)
1332             ->where($this->_db->quoteIdentifier('creation_time') . " is not null")
1333             ->order('creation_time ASC')
1334             ->limit(1);
1335         $creationTime = $this->_db->fetchOne($select);
1336
1337         $result = (!empty($creationTime)) ? new Tinebase_DateTime($creationTime) : $fallback;
1338         return $result;
1339     }
1340
1341     /**
1342      * checks if use table already has modlog fields
1343      *
1344      * @return bool
1345      */
1346     protected function _userTableHasModlogFields()
1347     {
1348         $schema = Tinebase_Db_Table::getTableDescriptionFromCache($this->_db->table_prefix . $this->_tableName, $this->_db);
1349         return isset($schema['creation_time']);
1350     }
1351
1352     /**
1353      * fetch all user ids from accounts table: updating from an old version fails if the modlog fields don't exist
1354      *
1355      * @return array
1356      */
1357     public function getAllUserIdsFromSqlBackend()
1358     {
1359         $sqlbackend = new Tinebase_Backend_Sql(array(
1360             'modelName' => 'Tinebase_Model_FullUser',
1361             'tableName' => $this->_tableName,
1362             'modlogActive' => true,
1363         ));
1364
1365         $userIds = $sqlbackend->search(null, null, Tinebase_Backend_Sql_Abstract::IDCOL);
1366         return $userIds;
1367     }
1368
1369     /**
1370      * get user by property from backend
1371      *
1372      * @param   string  $_property      the key to filter
1373      * @param   string  $_value         the value to search for
1374      * @param   string  $_accountClass  type of model to return
1375      *
1376      * @return  Tinebase_Model_User the user object
1377      */
1378     public function getUserByPropertyFromBackend($_property, $_value, $_accountClass = 'Tinebase_Model_User')
1379     {
1380         return $this->getUserByPropertyFromSqlBackend($_property, $_value, $_accountClass);
1381     }
1382
1383     /**
1384      * @param Tinebase_Model_ModificationLog $modification
1385      */
1386     public function applyReplicationModificationLog(Tinebase_Model_ModificationLog $modification)
1387     {
1388         switch ($modification->change_type) {
1389             case Tinebase_Timemachine_ModificationLog::CREATED:
1390                 $diff = new Tinebase_Record_Diff(json_decode($modification->new_value, true));
1391                 $record = new Tinebase_Model_FullUser($diff->diff);
1392                 $this->addUser($record);
1393                 break;
1394
1395             case Tinebase_Timemachine_ModificationLog::UPDATED:
1396                 $diff = new Tinebase_Record_Diff(json_decode($modification->new_value, true));
1397
1398                 if (isset($diff->diff['password'])) {
1399                     $diffArray = $diff->diff;
1400                     $oldDataArray = $diff->oldData;
1401                     $this->setPassword($modification->record_id, $diffArray['password'], false);
1402                     unset($diffArray['password']);
1403                     unset($diffArray['last_password_change']);
1404                     unset($oldDataArray['password']);
1405                     unset($oldDataArray['last_password_change']);
1406                     $diff->diff = $diffArray;
1407                     $diff->oldData = $oldDataArray;
1408                 }
1409
1410                 if (!$diff->isEmpty()) {
1411                     $record = $this->getUserById($modification->record_id, 'Tinebase_Model_FullUser');
1412                     $record->applyDiff($diff);
1413                     $this->updateUser($record);
1414                 }
1415                 break;
1416
1417             case Tinebase_Timemachine_ModificationLog::DELETED:
1418                 $this->deleteUser($modification->record_id);
1419                 break;
1420
1421             default:
1422                 throw new Tinebase_Exception('unknown Tinebase_Model_ModificationLog->old_value: ' . $modification->old_value);
1423         }
1424     }
1425 }