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