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