f98cbebe8af528e0de661316fe2fcb746d11aa77
[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-2013 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     /**
23      * row name mapping 
24      * 
25      * @var array
26      */
27     protected $rowNameMapping = array(
28         'accountId'                 => 'id',
29         'accountDisplayName'        => 'display_name',
30         'accountFullName'           => 'full_name',
31         'accountFirstName'          => 'first_name',
32         'accountLastName'           => 'last_name',
33         'accountLoginName'          => 'login_name',
34         'accountLastLogin'          => 'last_login',
35         'accountLastLoginfrom'      => 'last_login_from',
36         'accountLastPasswordChange' => 'last_password_change',
37         'accountStatus'             => 'status',
38         'accountExpires'            => 'expires_at',
39         'accountPrimaryGroup'       => 'primary_group_id',
40         'accountEmailAddress'       => 'email',
41         'accountHomeDirectory'      => 'home_dir',
42         'accountLoginShell'         => 'login_shell',
43         'lastLoginFailure'          => 'last_login_failure_at',
44         'loginFailures'             => 'login_failures',
45         'openid'                    => 'openid',
46         'visibility'                => 'visibility',
47         'contactId'                 => 'contact_id'
48     );
49     
50     /**
51      * copy of Tinebase_Core::get('dbAdapter')
52      *
53      * @var Zend_Db_Adapter_Abstract
54      */
55     protected $_db;
56     
57     /**
58      * sql user plugins
59      * 
60      * @var array
61      */
62     protected $_sqlPlugins = array();
63     
64     /**
65      * Table name without prefix
66      *
67      * @var string
68      */
69     protected $_tableName = 'accounts';
70     
71     /**
72      * @var Tinebase_Backend_Sql_Command_Interface
73      */
74     protected $_dbCommand;
75
76     /**
77      * the constructor
78      *
79      * @param  array $options Options used in connecting, binding, etc.
80      */
81     public function __construct(array $_options = array())
82     {
83         parent::__construct($_options);
84
85         $this->_db = Tinebase_Core::getDb();
86         $this->_dbCommand = Tinebase_Backend_Sql_Command::factory($this->_db);
87     }
88
89     /**
90      * registerPlugins
91      * 
92      * @param array $plugins
93      */
94     public function registerPlugins($plugins)
95     {
96         parent::registerPlugins($plugins);
97         
98         foreach ($this->_plugins as $plugin) {
99             if ($plugin instanceof Tinebase_User_Plugin_SqlInterface) {
100                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
101                     . " Registering " . get_class($plugin) . ' SQL plugin.');
102                 $this->_sqlPlugins[] = $plugin;
103             }
104         }
105     }
106     
107     /**
108      * unregisterAllPlugins
109      */
110     public function unregisterAllPlugins()
111     {
112         parent::unregisterAllPlugins();
113         $this->_sqlPlugins = array();
114     }
115     
116     /**
117      * get list of users
118      *
119      * @param string $_filter
120      * @param string $_sort
121      * @param string $_dir
122      * @param int $_start
123      * @param int $_limit
124      * @param string $_accountClass the type of subclass for the Tinebase_Record_RecordSet to return
125      * @return Tinebase_Record_RecordSet with record class Tinebase_Model_User
126      */
127     public function getUsers($_filter = NULL, $_sort = NULL, $_dir = 'ASC', $_start = NULL, $_limit = NULL, $_accountClass = 'Tinebase_Model_User')
128     {
129         $select = $this->_getUserSelectObject()
130             ->limit($_limit, $_start);
131             
132         if ($_sort !== NULL && isset($this->rowNameMapping[$_sort])) {
133             $select->order($this->_db->table_prefix . $this->_tableName . '.' . $this->rowNameMapping[$_sort] . ' ' . $_dir);
134         }
135
136         if (!empty($_filter)) {
137             $whereStatement = array();
138             $defaultValues = array(
139                 $this->rowNameMapping['accountLastName'], 
140                 $this->rowNameMapping['accountFirstName'], 
141                 $this->rowNameMapping['accountLoginName']
142             );
143             // prepare for case insensitive search
144             $db = Tinebase_Core::getDb();
145             foreach ($defaultValues as $defaultValue) {
146                 $whereStatement[] = Tinebase_Backend_Sql_Command::factory($db)->prepareForILike($this->_db->quoteIdentifier($defaultValue)) . ' LIKE ' . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike('?');
147             }
148         
149             $select->where('(' . implode(' OR ', $whereStatement) . ')', '%' . $_filter . '%');
150         }
151         
152         // @todo still needed?? either we use contacts from addressboook or full users now
153         // return only active users, when searching for simple users
154         if ($_accountClass == 'Tinebase_Model_User') {
155             $select->where($this->_db->quoteInto($this->_db->quoteIdentifier('status') . ' = ?', 'enabled'));
156         }
157
158         $stmt = $select->query();
159
160         $rows = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
161
162         $result = new Tinebase_Record_RecordSet($_accountClass, $rows, TRUE);
163         
164         return $result;
165     }
166         
167     /**
168      * get user by property
169      *
170      * @param   string  $_property      the key to filter
171      * @param   string  $_value         the value to search for
172      * @param   string  $_accountClass  type of model to return
173      * 
174      * @return  Tinebase_Model_User the user object
175      */
176     public function getUserByProperty($_property, $_value, $_accountClass = 'Tinebase_Model_User')
177     {
178         $user = $this->getUserByPropertyFromSqlBackend($_property, $_value, $_accountClass);
179         
180         // append data from plugins
181         foreach ($this->_sqlPlugins as $plugin) {
182             try {
183                 $plugin->inspectGetUserByProperty($user);
184             } catch (Tinebase_Exception_NotFound $tenf) {
185                 // do nothing
186             } catch (Exception $e) {
187                 if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . ' User sql plugin failure: ' . $e);
188             }
189         }
190             
191         if ($this instanceof Tinebase_User_Interface_SyncAble) {
192             try {
193                 $syncUser = $this->getUserByPropertyFromSyncBackend('accountId', $user, $_accountClass);
194                 
195                 if (!empty($syncUser->emailUser)) {
196                     $user->emailUser  = $syncUser->emailUser;
197                 }
198                 if (!empty($syncUser->imapUser)) {
199                     $user->imapUser  = $syncUser->imapUser;
200                 }
201                 if (!empty($syncUser->smtpUser)) {
202                     $user->smtpUser  = $syncUser->smtpUser;
203                 }
204                 if (!empty($syncUser->sambaSAM)) {
205                     $user->sambaSAM  = $syncUser->sambaSAM;
206                 }
207             } catch (Tinebase_Exception_NotFound $tenf) {
208                 if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . ' user not found in sync backend: ' . $user->getId());
209             }
210         }
211         
212         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($user->toArray(), true));
213         
214         return $user;
215     }
216
217     /**
218      * get user by property
219      *
220      * @param   string  $_property      the key to filter
221      * @param   string  $_value         the value to search for
222      * @param   string  $_accountClass  type of model to return
223      * 
224      * @return  Tinebase_Model_User the user object
225      * @throws  Tinebase_Exception_NotFound
226      * @throws  Tinebase_Exception_Record_Validation
227      */
228     public function getUserByPropertyFromSqlBackend($_property, $_value, $_accountClass = 'Tinebase_Model_User')
229     {
230         if(!(isset($this->rowNameMapping[$_property]) || array_key_exists($_property, $this->rowNameMapping))) {
231             throw new Tinebase_Exception_InvalidArgument("invalid property $_property requested");
232         }
233         
234         switch($_property) {
235             case 'accountId':
236                 $value = Tinebase_Model_User::convertUserIdToInt($_value);
237                 break;
238             default:
239                 $value = $_value;
240                 break;
241         }
242         
243         $select = $this->_getUserSelectObject()
244             ->where($this->_db->quoteInto($this->_db->quoteIdentifier( SQL_TABLE_PREFIX . 'accounts.' . $this->rowNameMapping[$_property]) . ' = ?', $value));
245         
246         $stmt = $select->query();
247
248         $row = $stmt->fetch(Zend_Db::FETCH_ASSOC);
249         if ($row === false) {
250             throw new Tinebase_Exception_NotFound('User with ' . $_property . ' = ' . $value . ' not found.');
251         }
252         
253         try {
254             $account = new $_accountClass(NULL, TRUE);
255             $account->setFromArray($row);
256         } catch (Tinebase_Exception_Record_Validation $e) {
257             $validation_errors = $account->getValidationErrors();
258             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . $e->getMessage() . "\n" .
259                 "Tinebase_Model_User::validation_errors: \n" .
260                 print_r($validation_errors,true));
261             throw ($e);
262         }
263         
264         return $account;
265     }
266     
267     /**
268      * get users by primary group
269      * 
270      * @param string $groupId
271      * @return Tinebase_Record_RecordSet of Tinebase_Model_FullUser
272      */
273     public function getUsersByPrimaryGroup($groupId)
274     {
275         $select = $this->_getUserSelectObject()
276             ->where($this->_db->quoteInto($this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.primary_group_id') . ' = ?', $groupId));
277         $stmt = $select->query();
278         $data = (array) $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
279         $result = new Tinebase_Record_RecordSet('Tinebase_Model_FullUser', $data, true);
280         return $result;
281     }
282     
283     /**
284      * get full user by id
285      *
286      * @param   int         $_accountId
287      * @return  Tinebase_Model_FullUser full user
288      */
289     public function getFullUserById($_accountId)
290     {
291         return $this->getUserById($_accountId, 'Tinebase_Model_FullUser');
292     }
293     
294     /**
295      * get user select
296      *
297      * @return Zend_Db_Select
298      */
299     protected function _getUserSelectObject()
300     {
301         /*
302          * CASE WHEN `status` = 'enabled' THEN (CASE WHEN NOW() > `expires_at` THEN 'expired' 
303          * WHEN (`login_failures` > 5 AND `last_login_failure_at` + INTERVAL 15 MINUTE > NOW()) 
304          * THEN 'blocked' ELSE 'enabled' END) ELSE 'disabled' END
305          */
306         
307         $maxLoginFailures = Tinebase_Config::getInstance()->get(Tinebase_Config::MAX_LOGIN_FAILURES, 5);
308         if ($maxLoginFailures > 0) {
309             $loginFailuresCondition = 'WHEN ( ' . $this->_db->quoteIdentifier($this->rowNameMapping['loginFailures']) . " > {$maxLoginFailures} AND "
310                 . $this->_dbCommand->setDate($this->_db->quoteIdentifier($this->rowNameMapping['lastLoginFailure'])) . " + INTERVAL '{$this->_blockTime}' MINUTE > "
311                 . $this->_dbCommand->setDate('NOW()') .") THEN 'blocked'";
312         } else {
313             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
314                 . ' User blocking disabled.');
315             $loginFailuresCondition = '';
316         }
317         
318         $statusSQL = 'CASE WHEN ' . $this->_db->quoteIdentifier($this->rowNameMapping['accountStatus']) . ' = ' . $this->_db->quote('enabled') . ' THEN (';
319         $statusSQL .= 'CASE WHEN '.$this->_dbCommand->setDate('NOW()') .' > ' . $this->_db->quoteIdentifier($this->rowNameMapping['accountExpires'])
320             . ' THEN ' . $this->_db->quote('expired')
321             . $loginFailuresCondition
322             . ' ELSE ' . $this->_db->quote('enabled') . ' END) ELSE ' . $this->_db->quote('disabled') . ' END';
323         
324         $select = $this->_db->select()
325             ->from(SQL_TABLE_PREFIX . 'accounts', 
326                 array(
327                     'accountId'             => $this->rowNameMapping['accountId'],
328                     'accountLoginName'      => $this->rowNameMapping['accountLoginName'],
329                     'accountLastLogin'      => $this->rowNameMapping['accountLastLogin'],
330                     'accountLastLoginfrom'  => $this->rowNameMapping['accountLastLoginfrom'],
331                     'accountLastPasswordChange' => $this->rowNameMapping['accountLastPasswordChange'],
332                     'accountStatus'         => $statusSQL,
333                     'accountExpires'        => $this->rowNameMapping['accountExpires'],
334                     'accountPrimaryGroup'   => $this->rowNameMapping['accountPrimaryGroup'],
335                     'accountHomeDirectory'  => $this->rowNameMapping['accountHomeDirectory'],
336                     'accountLoginShell'     => $this->rowNameMapping['accountLoginShell'],
337                     'accountDisplayName'    => $this->rowNameMapping['accountDisplayName'],
338                     'accountFullName'       => $this->rowNameMapping['accountFullName'],
339                     'accountFirstName'      => $this->rowNameMapping['accountFirstName'],
340                     'accountLastName'       => $this->rowNameMapping['accountLastName'],
341                     'accountEmailAddress'   => $this->rowNameMapping['accountEmailAddress'],
342                     'lastLoginFailure'      => $this->rowNameMapping['lastLoginFailure'],
343                     'loginFailures'         => $this->rowNameMapping['loginFailures'],
344                     'contact_id',
345                     'openid',
346                     'visibility'
347                 )
348             )
349             ->joinLeft(
350                SQL_TABLE_PREFIX . 'addressbook',
351                $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.contact_id') . ' = ' 
352                 . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'addressbook.id'), 
353                 array(
354                     'container_id'            => 'container_id'
355                 )
356             );
357         
358         return $select;
359     }
360     
361     /**
362      * set the password for given account
363      *
364      * @param   string  $_userId
365      * @param   string  $_password
366      * @param   bool    $_encrypt encrypt password
367      * @param   bool    $_mustChange
368      * @return  void
369      * @throws  Tinebase_Exception_InvalidArgument
370      */
371     public function setPassword($_userId, $_password, $_encrypt = TRUE, $_mustChange = null)
372     {
373         $userId = $_userId instanceof Tinebase_Model_User ? $_userId->getId() : $_userId;
374         $user = $_userId instanceof Tinebase_Model_FullUser ? $_userId : $this->getFullUserById($userId);
375         $this->checkPasswordPolicy($_password, $user);
376         
377         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
378         
379         $accountData['password'] = ($_encrypt) ? Hash_Password::generate('SSHA256', $_password) : $_password;
380         $accountData['last_password_change'] = Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG);
381         
382         $where = array(
383             $accountsTable->getAdapter()->quoteInto($accountsTable->getAdapter()->quoteIdentifier('id') . ' = ?', $userId)
384         );
385         
386         $result = $accountsTable->update($accountData, $where);
387         
388         if ($result != 1) {
389             throw new Tinebase_Exception_NotFound('Unable to update password! account not found in authentication backend.');
390         }
391         
392         $this->_setPluginsPassword($userId, $_password, $_encrypt);
393     }
394     
395     /**
396      * set password in plugins
397      * 
398      * @param string $userId
399      * @param string $password
400      * @param bool   $encrypt encrypt password
401      * @throws Tinebase_Exception_Backend
402      */
403     protected function _setPluginsPassword($userId, $password, $encrypt = TRUE)
404     {
405         foreach ($this->_sqlPlugins as $plugin) {
406             try {
407                 $plugin->inspectSetPassword($userId, $password, $encrypt);
408             } catch (Exception $e) {
409                 Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Could not change plugin password: ' . $e);
410                 throw new Tinebase_Exception_Backend($e->getMessage());
411             }
412         }
413     }
414     
415     /**
416      * ensure password policy
417      * 
418      * @param string $password
419      * @param Tinebase_Model_FullUser $user
420      * @throws Tinebase_Exception_PasswordPolicyViolation
421      */
422     public function checkPasswordPolicy($password, Tinebase_Model_FullUser $user)
423     {
424         if (! Tinebase_Config::getInstance()->get(Tinebase_Config::PASSWORD_POLICY_ACTIVE, FALSE)) {
425             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
426                 . ' No password policy enabled');
427             return;
428         }
429         
430         $failedTests = array();
431         
432         $policy = array(
433             Tinebase_Config::PASSWORD_POLICY_ONLYASCII              => '/[^\x00-\x7F]/',
434             Tinebase_Config::PASSWORD_POLICY_MIN_LENGTH             => NULL,
435             Tinebase_Config::PASSWORD_POLICY_MIN_WORD_CHARS         => '/[\W]*/',
436             Tinebase_Config::PASSWORD_POLICY_MIN_UPPERCASE_CHARS    => '/[^A-Z]*/',
437             Tinebase_Config::PASSWORD_POLICY_MIN_SPECIAL_CHARS      => '/[\w]*/',
438             Tinebase_Config::PASSWORD_POLICY_MIN_NUMBERS            => '/[^0-9]*/',
439             Tinebase_Config::PASSWORD_POLICY_FORBID_USERNAME        => $user->accountLoginName,
440         );
441         
442         foreach ($policy as $key => $regex) {
443             $test = $this->_testPolicy($password, $key, $regex);
444             if ($test !== TRUE) {
445                 $failedTests[$key] = $test;
446             }
447         }
448         
449         if (! empty($failedTests)) {
450             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
451                 . ' ' . print_r($failedTests, TRUE));
452             
453             $policyException = new Tinebase_Exception_PasswordPolicyViolation('Password failed to match the following policy requirements: ' 
454                 . implode('|', array_keys($failedTests)));
455             throw $policyException;
456         }
457     }
458     
459     /**
460      * test password policy
461      * 
462      * @param string $password
463      * @param string $configKey
464      * @param string $regex
465      * @return mixed
466      */
467     protected function _testPolicy($password, $configKey, $regex = NULL)
468     {
469         $result = TRUE;
470         
471         if ($configKey === Tinebase_Config::PASSWORD_POLICY_ONLYASCII && Tinebase_Config::getInstance()->get($configKey, 0) && $regex !== NULL) {
472             $nonAsciiFound = preg_match($regex, $password, $matches);
473             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
474                 . ' ' . print_r($matches, TRUE));
475             
476             $result = ($nonAsciiFound) ? array('expected' => 0, 'got' => count($matches)) : TRUE;
477         } else if ($configKey === Tinebase_Config::PASSWORD_POLICY_FORBID_USERNAME) {
478             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
479                 . ' Testing if password is part of username "' . $regex . '"');
480             if (! empty($password)) {
481                 $result = ! preg_match('/' . preg_quote($password) . '/i', $regex);
482             }
483         } else {
484             // check min length restriction
485             $minLength = Tinebase_Config::getInstance()->get($configKey, 0);
486             if ($minLength > 0) {
487                 $reduced = ($regex) ? preg_replace($regex, '', $password) : $password;
488                 $charCount = strlen(utf8_decode($reduced));
489                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
490                     . ' Found ' . $charCount . '/' . $minLength . ' chars for ' . $configKey /*. ': ' . $reduced */);
491                 
492                 if ($charCount < $minLength) {
493                     $result = array('expected' => $minLength, 'got' => $charCount);
494                 }
495             }
496         }
497         
498         return $result;
499     }
500     
501     /**
502      * set the status of the user
503      *
504      * @param mixed   $_accountId
505      * @param string  $_status
506      * @return unknown
507      */
508     public function setStatus($_accountId, $_status)
509     {
510         if($this instanceof Tinebase_User_Interface_SyncAble) {
511             $this->setStatusInSyncBackend($_accountId, $_status);
512         }
513         
514         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
515         
516         switch($_status) {
517             case 'enabled':
518                 $accountData[$this->rowNameMapping['loginFailures']]  = 0;
519                 $accountData[$this->rowNameMapping['accountExpires']] = null;
520                 $accountData['status'] = $_status;
521                 break;
522                 
523             case 'disabled':
524                 $accountData['status'] = $_status;
525                 break;
526                 
527             case 'expired':
528                 $accountData['expires_at'] = Tinebase_DateTime::getTimestamp();
529                 break;
530             
531             default:
532                 throw new Tinebase_Exception_InvalidArgument('$_status can be only enabled, disabled or expired');
533                 break;
534         }
535         
536         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
537
538         $where = array(
539             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
540         );
541         
542         $result = $accountsTable->update($accountData, $where);
543         
544         return $result;
545     }
546
547     /**
548      * sets/unsets expiry date 
549      *
550      * @param     mixed      $_accountId
551      * @param     Tinebase_DateTime  $_expiryDate set to NULL to disable expirydate
552     */
553     public function setExpiryDate($_accountId, $_expiryDate)
554     {
555         if($this instanceof Tinebase_User_Interface_SyncAble) {
556             $this->setExpiryDateInSyncBackend($_accountId, $_expiryDate);
557         }
558         
559         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
560         
561         if($_expiryDate instanceof DateTime) {
562             $accountData['expires_at'] = $_expiryDate->get(Tinebase_Record_Abstract::ISO8601LONG);
563         } else {
564             $accountData['expires_at'] = NULL;
565         }
566         
567         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
568
569         $where = array(
570             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
571         );
572         
573         $result = $accountsTable->update($accountData, $where);
574         
575         return $result;
576     }
577     
578     /**
579      * set last login failure in accounts table
580      * 
581      * @param string $_loginName
582      * @see Tinebase/User/Tinebase_User_Interface::setLastLoginFailure()
583      */
584     public function setLastLoginFailure($_loginName)
585     {
586         Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Login of user ' . $_loginName . ' failed.');
587         
588         try {
589             $user = $this->getUserByLoginName($_loginName);
590         } catch (Tinebase_Exception_NotFound $tenf) {
591             // nothing todo => is no existing user
592             return;
593         }
594         
595         $values = array(
596             'last_login_failure_at' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
597             'login_failures'        => new Zend_Db_Expr($this->_db->quoteIdentifier('login_failures') . ' + 1')
598         );
599         
600         $where = array(
601             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId())
602         );
603         
604         $this->_db->update(SQL_TABLE_PREFIX . 'accounts', $values, $where);
605     }
606     
607     /**
608      * update the lastlogin time of user
609      *
610      * @param int $_accountId
611      * @param string $_ipAddress
612      * @return void
613      */
614     public function setLoginTime($_accountId, $_ipAddress) 
615     {
616         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
617         
618         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
619         
620         $accountData['last_login_from'] = $_ipAddress;
621         $accountData['last_login']      = Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG);
622         $accountData['login_failures']  = 0;
623         
624         $where = array(
625             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
626         );
627         
628         $result = $accountsTable->update($accountData, $where);
629         
630         return $result;
631     }
632     
633     /**
634      * update contact data(first name, last name, ...) of user
635      * 
636      * @param Addressbook_Model_Contact $contact
637      */
638     public function updateContact(Addressbook_Model_Contact $_contact)
639     {
640         if($this instanceof Tinebase_User_Interface_SyncAble) {
641             $this->updateContactInSyncBackend($_contact);
642         }
643         
644         return $this->updateContactInSqlBackend($_contact);
645     }
646     
647     /**
648      * update contact data(first name, last name, ...) of user in local sql storage
649      * 
650      * @param Addressbook_Model_Contact $contact
651      */
652     public function updateContactInSqlBackend(Addressbook_Model_Contact $_contact)
653     {
654         $contactId = $_contact->getId();
655
656         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
657
658         $accountData = array(
659             $this->rowNameMapping['accountDisplayName']  => $_contact->n_fileas,
660             $this->rowNameMapping['accountFullName']     => $_contact->n_fn,
661             $this->rowNameMapping['accountFirstName']    => $_contact->n_given,
662             $this->rowNameMapping['accountLastName']     => $_contact->n_family,
663             $this->rowNameMapping['accountEmailAddress'] => $_contact->email
664         );
665         
666         try {
667             $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
668             
669             $where = array(
670                 $this->_db->quoteInto($this->_db->quoteIdentifier('contact_id') . ' = ?', $contactId)
671             );
672             $accountsTable->update($accountData, $where);
673
674         } catch (Exception $e) {
675             Tinebase_TransactionManager::getInstance()->rollBack();
676             throw($e);
677         }
678     }
679     
680     /**
681      * updates an user
682      * 
683      * this function updates an user 
684      *
685      * @param Tinebase_Model_FullUser $_user
686      * @return Tinebase_Model_FullUser
687      */
688     public function updateUser(Tinebase_Model_FullUser $_user)
689     {
690         if($this instanceof Tinebase_User_Interface_SyncAble) {
691             $this->updateUserInSyncBackend($_user);
692         }
693         
694         $updatedUser = $this->updateUserInSqlBackend($_user);
695         $this->updatePluginUser($updatedUser, $_user);
696         
697         return $updatedUser;
698     }
699     
700     /**
701     * update data in plugins
702     *
703     * @param Tinebase_Model_FullUser $updatedUser
704     * @param Tinebase_Model_FullUser $newUserProperties
705     */
706     public function updatePluginUser($updatedUser, $newUserProperties)
707     {
708         foreach ($this->_sqlPlugins as $plugin) {
709             $plugin->inspectUpdateUser($updatedUser, $newUserProperties);
710         }
711     }
712     
713     /**
714      * updates an user
715      * 
716      * this function updates an user 
717      *
718      * @param Tinebase_Model_FullUser $_user
719      * @return Tinebase_Model_FullUser
720      * @throws 
721      */
722     public function updateUserInSqlBackend(Tinebase_Model_FullUser $_user)
723     {
724         if(! $_user->isValid()) {
725             throw new Tinebase_Exception_Record_Validation('Invalid user object. ' . print_r($_user->getValidationErrors(), TRUE));
726         }
727
728         $accountId = Tinebase_Model_User::convertUserIdToInt($_user);
729
730         $oldUser = $this->getFullUserById($accountId);
731         
732         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
733         
734         if (empty($_user->contact_id)) {
735             $_user->visibility = 'hidden';
736             $_user->contact_id = null;
737         }
738         
739         $accountData = array(
740             'login_name'        => $_user->accountLoginName,
741             'expires_at'        => ($_user->accountExpires instanceof DateTime ? $_user->accountExpires->get(Tinebase_Record_Abstract::ISO8601LONG) : NULL),
742             'primary_group_id'  => $_user->accountPrimaryGroup,
743             'home_dir'          => $_user->accountHomeDirectory,
744             'login_shell'       => $_user->accountLoginShell,
745             'openid'            => $_user->openid,
746             'visibility'        => $_user->visibility,
747             'contact_id'        => $_user->contact_id,
748             $this->rowNameMapping['accountDisplayName']  => $_user->accountDisplayName,
749             $this->rowNameMapping['accountFullName']     => $_user->accountFullName,
750             $this->rowNameMapping['accountFirstName']    => $_user->accountFirstName,
751             $this->rowNameMapping['accountLastName']     => $_user->accountLastName,
752             $this->rowNameMapping['accountEmailAddress'] => $_user->accountEmailAddress,
753         );
754         
755         // ignore all other states (expired and blocked)
756         if ($_user->accountStatus == Tinebase_User::STATUS_ENABLED) {
757             $accountData[$this->rowNameMapping['accountStatus']] = $_user->accountStatus;
758             
759             if ($oldUser->accountStatus === Tinebase_User::STATUS_BLOCKED) {
760                 $accountData[$this->rowNameMapping['loginFailures']] = 0;
761             } elseif ($oldUser->accountStatus === Tinebase_User::STATUS_EXPIRED) {
762                 $accountData[$this->rowNameMapping['accountExpires']] = null;
763             }
764         } elseif ($_user->accountStatus == Tinebase_User::STATUS_DISABLED) {
765             $accountData[$this->rowNameMapping['accountStatus']] = $_user->accountStatus;
766         }
767         
768         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($accountData, true));
769
770         try {
771             $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
772             
773             $where = array(
774                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
775             );
776             $accountsTable->update($accountData, $where);
777             
778         } catch (Exception $e) {
779             Tinebase_TransactionManager::getInstance()->rollBack();
780             throw($e);
781         }
782         
783         return $this->getUserById($accountId, 'Tinebase_Model_FullUser');
784     }
785     
786     /**
787      * add an user
788      * 
789      * @param   Tinebase_Model_FullUser  $_user
790      * @return  Tinebase_Model_FullUser
791      */
792     public function addUser(Tinebase_Model_FullUser $_user)
793     {
794         if ($this instanceof Tinebase_User_Interface_SyncAble) {
795             $userFromSyncBackend = $this->addUserToSyncBackend($_user);
796             if ($userFromSyncBackend !== NULL) {
797                 // set accountId for sql backend sql backend
798                 $_user->setId($userFromSyncBackend->getId());
799             }
800         }
801         
802         $addedUser = $this->addUserInSqlBackend($_user);
803         $this->addPluginUser($addedUser, $_user);
804         
805         return $addedUser;
806     }
807     
808     /**
809      * add data from/to plugins
810      * 
811      * @param Tinebase_Model_FullUser $addedUser
812      * @param Tinebase_Model_FullUser $newUserProperties
813      */
814     public function addPluginUser($addedUser, $newUserProperties)
815     {
816         foreach ($this->_sqlPlugins as $plugin) {
817             $plugin->inspectAddUser($addedUser, $newUserProperties);
818         }
819     }
820     
821     /**
822      * add an user
823      * 
824      * @todo fix $contactData['container_id'] = 1;
825      *
826      * @param   Tinebase_Model_FullUser  $_user
827      * @return  Tinebase_Model_FullUser
828      */
829     public function addUserInSqlBackend(Tinebase_Model_FullUser $_user)
830     {
831         $_user->isValid(TRUE);
832         
833         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
834         
835         if(!isset($_user->accountId)) {
836             $userId = $_user->generateUID();
837             $_user->setId($userId);
838         }
839         
840         if (empty($_user->contact_id)) {
841             $_user->visibility = 'hidden';
842             $_user->contact_id = null;
843         }
844         
845         $accountData = array(
846             'id'                => $_user->accountId,
847             'login_name'        => $_user->accountLoginName,
848             'status'            => $_user->accountStatus,
849             'expires_at'        => ($_user->accountExpires instanceof DateTime ? $_user->accountExpires->get(Tinebase_Record_Abstract::ISO8601LONG) : NULL),
850             'primary_group_id'  => $_user->accountPrimaryGroup,
851             'home_dir'          => $_user->accountHomeDirectory,
852             'login_shell'       => $_user->accountLoginShell,
853             'openid'            => $_user->openid,
854             'visibility'        => $_user->visibility, 
855             'contact_id'        => $_user->contact_id,
856             $this->rowNameMapping['accountDisplayName']  => $_user->accountDisplayName,
857             $this->rowNameMapping['accountFullName']     => $_user->accountFullName,
858             $this->rowNameMapping['accountFirstName']    => $_user->accountFirstName,
859             $this->rowNameMapping['accountLastName']     => $_user->accountLastName,
860             $this->rowNameMapping['accountEmailAddress'] => $_user->accountEmailAddress,
861         );
862         
863         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Adding user to SQL backend: ' . $_user->accountLoginName);
864         
865         $accountsTable->insert($accountData);
866             
867         return $this->getUserById($_user->getId(), 'Tinebase_Model_FullUser');
868     }
869     
870     /**
871      * delete a user
872      *
873      * @param  mixed  $_userId
874      */
875     public function deleteUser($_userId)
876     {
877         $deletedUser = $this->deleteUserInSqlBackend($_userId);
878         
879         if($this instanceof Tinebase_User_Interface_SyncAble) {
880             $this->deleteUserInSyncBackend($deletedUser);
881         }
882         
883         // update data from plugins
884         foreach ($this->_sqlPlugins as $plugin) {
885             $plugin->inspectDeleteUser($deletedUser);
886         }
887         
888     }
889     
890     /**
891      * delete a user
892      *
893      * @param  mixed  $_userId
894      * @return Tinebase_Model_FullUser  the delete user
895      */
896     public function deleteUserInSqlBackend($_userId)
897     {
898         if ($_userId instanceof Tinebase_Model_FullUser) {
899             $user = $_userId;
900         } else {
901             $user = $this->getFullUserById($_userId);
902         }
903         
904         $accountsTable          = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
905         $groupMembersTable      = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'group_members'));
906         $roleMembersTable       = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'role_accounts'));
907         $userRegistrationsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'registrations'));
908         
909         try {
910             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($this->_db);
911             
912             $where  = array(
913                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_id') . ' = ?', $user->getId()),
914             );
915             $groupMembersTable->delete($where);
916
917             $where  = array(
918                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_id')   . ' = ?', $user->getId()),
919                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_type') . ' = ?', Tinebase_Acl_Rights::ACCOUNT_TYPE_USER),
920             );
921             $roleMembersTable->delete($where);
922             
923             $where  = array(
924                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId()),
925             );
926             $accountsTable->delete($where);
927
928             $where  = array(
929                 $this->_db->quoteInto($this->_db->quoteIdentifier('login_name') . ' = ?', $user->accountLoginName),
930             );
931             $userRegistrationsTable->delete($where);
932             
933             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
934         } catch (Exception $e) {
935             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' error while deleting account ' . $e->__toString());
936             Tinebase_TransactionManager::getInstance()->rollBack();
937             throw($e);
938         }
939         
940         return $user;
941     }
942     
943     /**
944      * delete users
945      * 
946      * @param array $_accountIds
947      */
948     public function deleteUsers(array $_accountIds)
949     {
950         foreach ( $_accountIds as $accountId ) {
951             $this->deleteUser($accountId);
952         }
953     }
954     
955     /**
956      * Delete all users returned by {@see getUsers()} using {@see deleteUsers()}
957      * 
958      * @return void
959      */
960     public function deleteAllUsers()
961     {
962         // need to fetch FullUser because otherwise we would get only enabled accounts :/
963         $users = $this->getUsers(NULL, NULL, 'ASC', NULL, NULL, 'Tinebase_Model_FullUser');
964         
965         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Deleting ' . count($users) .' users');
966         foreach ( $users as $user ) {
967             $this->deleteUser($user);
968         }
969     }
970
971     /**
972      * Get multiple users
973      *
974      * fetch FullUser by default
975      *
976      * @param  string|array $_id Ids
977      * @param  string  $_accountClass  type of model to return
978      * @return Tinebase_Record_RecordSet of 'Tinebase_Model_User' or 'Tinebase_Model_FullUser'
979      */
980     public function getMultiple($_id, $_accountClass = 'Tinebase_Model_FullUser') 
981     {
982         if (empty($_id)) {
983             return new Tinebase_Record_RecordSet($_accountClass);
984         }
985         
986         $select = $this->_getUserSelectObject()
987             ->where($this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.id') . ' in (?)', (array) $_id);
988         
989         $stmt = $this->_db->query($select);
990         $queryResult = $stmt->fetchAll();
991         
992         $result = new Tinebase_Record_RecordSet($_accountClass, $queryResult, TRUE);
993         
994         return $result;
995     }
996 }