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