Merge branch '2014.11' into 2015.11
[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');
220                 Tinebase_Exception::log($e);
221             }
222         }
223             
224         if ($this instanceof Tinebase_User_Interface_SyncAble) {
225             try {
226                 $syncUser = $this->getUserByPropertyFromSyncBackend('accountId', $user, $_accountClass);
227                 
228                 if (!empty($syncUser->emailUser)) {
229                     $user->emailUser  = $syncUser->emailUser;
230                 }
231                 if (!empty($syncUser->imapUser)) {
232                     $user->imapUser  = $syncUser->imapUser;
233                 }
234                 if (!empty($syncUser->smtpUser)) {
235                     $user->smtpUser  = $syncUser->smtpUser;
236                 }
237                 if (!empty($syncUser->sambaSAM)) {
238                     $user->sambaSAM  = $syncUser->sambaSAM;
239                 }
240             } catch (Tinebase_Exception_NotFound $tenf) {
241                 if (Tinebase_Core::isLogLevel(Zend_Log::CRIT)) Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . ' user not found in sync backend: ' . $user->getId());
242             }
243         }
244         
245         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($user->toArray(), true));
246         
247         return $user;
248     }
249
250     /**
251      * get user by property
252      *
253      * @param   string  $_property      the key to filter
254      * @param   string  $_value         the value to search for
255      * @param   string  $_accountClass  type of model to return
256      * 
257      * @return  Tinebase_Model_User the user object
258      * @throws  Tinebase_Exception_NotFound
259      * @throws  Tinebase_Exception_Record_Validation
260      * @throws  Tinebase_Exception_InvalidArgument
261      */
262     public function getUserByPropertyFromSqlBackend($_property, $_value, $_accountClass = 'Tinebase_Model_User')
263     {
264         if(!(isset($this->rowNameMapping[$_property]) || array_key_exists($_property, $this->rowNameMapping))) {
265             throw new Tinebase_Exception_InvalidArgument("invalid property $_property requested");
266         }
267         
268         switch($_property) {
269             case 'accountId':
270                 $value = Tinebase_Model_User::convertUserIdToInt($_value);
271                 break;
272             default:
273                 $value = $_value;
274                 break;
275         }
276         
277         $select = $this->_getUserSelectObject()
278             ->where($this->_db->quoteInto($this->_db->quoteIdentifier( SQL_TABLE_PREFIX . 'accounts.' . $this->rowNameMapping[$_property]) . ' = ?', $value));
279
280         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $select);
281
282         $stmt = $select->query();
283
284         $row = $stmt->fetch(Zend_Db::FETCH_ASSOC);
285         if ($row === false) {
286             throw new Tinebase_Exception_NotFound('User with ' . $_property . ' = ' . $value . ' not found.');
287         }
288
289         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($row, true));
290
291         try {
292             $account = new $_accountClass(NULL, TRUE);
293             $account->setFromArray($row);
294         } catch (Tinebase_Exception_Record_Validation $e) {
295             $validation_errors = $account->getValidationErrors();
296             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . $e->getMessage() . "\n" .
297                 "Tinebase_Model_User::validation_errors: \n" .
298                 print_r($validation_errors,true));
299             throw ($e);
300         }
301         
302         return $account;
303     }
304     
305     /**
306      * get users by primary group
307      * 
308      * @param string $groupId
309      * @return Tinebase_Record_RecordSet of Tinebase_Model_FullUser
310      */
311     public function getUsersByPrimaryGroup($groupId)
312     {
313         $select = $this->_getUserSelectObject()
314             ->where($this->_db->quoteInto($this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.primary_group_id') . ' = ?', $groupId));
315         $stmt = $select->query();
316         $data = (array) $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
317         $result = new Tinebase_Record_RecordSet('Tinebase_Model_FullUser', $data, true);
318         return $result;
319     }
320     
321     /**
322      * get full user by id
323      *
324      * @param   int         $_accountId
325      * @return  Tinebase_Model_FullUser full user
326      */
327     public function getFullUserById($_accountId)
328     {
329         return $this->getUserById($_accountId, 'Tinebase_Model_FullUser');
330     }
331     
332     /**
333      * get user select
334      *
335      * @return Zend_Db_Select
336      */
337     protected function _getUserSelectObject()
338     {
339         $interval = $this->_dbCommand->getDynamicInterval(
340             'SECOND',
341             '1',
342             'CASE WHEN ' . $this->_db->quoteIdentifier($this->rowNameMapping['loginFailures'])
343             . ' > 5 THEN 60 ELSE POWER(2, ' . $this->_db->quoteIdentifier($this->rowNameMapping['loginFailures']) . ') END');
344         
345         $statusSQL = 'CASE WHEN ' . $this->_db->quoteIdentifier($this->rowNameMapping['accountStatus']) . ' = ' . $this->_db->quote('enabled') . ' THEN ('
346             . 'CASE WHEN '.$this->_dbCommand->setDate('NOW()') .' > ' . $this->_db->quoteIdentifier($this->rowNameMapping['accountExpires'])
347             . ' THEN ' . $this->_db->quote('expired')
348             . ' WHEN ( ' . $this->_db->quoteIdentifier($this->rowNameMapping['loginFailures']) . ' > 0 AND '
349             . $this->_db->quoteIdentifier($this->rowNameMapping['lastLoginFailure']) . ' + ' . $interval . ' > NOW()) THEN ' . $this->_db->quote('blocked')
350             . ' ELSE ' . $this->_db->quote('enabled') . ' END)'
351             . ' WHEN ' . $this->_db->quoteIdentifier($this->rowNameMapping['accountStatus']) . ' = ' . $this->_db->quote('expired')
352                 . ' THEN ' . $this->_db->quote('expired')
353             . ' ELSE ' . $this->_db->quote('disabled') . ' END';
354
355         $fields =  array(
356             'accountId'             => $this->rowNameMapping['accountId'],
357             'accountLoginName'      => $this->rowNameMapping['accountLoginName'],
358             'accountLastLogin'      => $this->rowNameMapping['accountLastLogin'],
359             'accountLastLoginfrom'  => $this->rowNameMapping['accountLastLoginfrom'],
360             'accountLastPasswordChange' => $this->rowNameMapping['accountLastPasswordChange'],
361             'accountStatus'         => $statusSQL,
362             'accountExpires'        => $this->rowNameMapping['accountExpires'],
363             'accountPrimaryGroup'   => $this->rowNameMapping['accountPrimaryGroup'],
364             'accountHomeDirectory'  => $this->rowNameMapping['accountHomeDirectory'],
365             'accountLoginShell'     => $this->rowNameMapping['accountLoginShell'],
366             'accountDisplayName'    => $this->rowNameMapping['accountDisplayName'],
367             'accountFullName'       => $this->rowNameMapping['accountFullName'],
368             'accountFirstName'      => $this->rowNameMapping['accountFirstName'],
369             'accountLastName'       => $this->rowNameMapping['accountLastName'],
370             'accountEmailAddress'   => $this->rowNameMapping['accountEmailAddress'],
371             'lastLoginFailure'      => $this->rowNameMapping['lastLoginFailure'],
372             'loginFailures'         => $this->rowNameMapping['loginFailures'],
373             'contact_id',
374             'openid',
375             'visibility',
376             'NOW()', // only needed for debugging
377         );
378
379         // modlog fields have been added later
380         if ($this->_userTableHasModlogFields()) {
381             $fields = array_merge($fields, array(
382                 'created_by',
383                 'creation_time',
384                 'last_modified_by',
385                 'last_modified_time',
386                 'is_deleted',
387                 'deleted_time',
388                 'deleted_by',
389                 'seq',
390             ));
391         }
392
393         $select = $this->_db->select()
394             ->from(SQL_TABLE_PREFIX . 'accounts', $fields)
395             ->joinLeft(
396                SQL_TABLE_PREFIX . 'addressbook',
397                $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.contact_id') . ' = ' 
398                 . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'addressbook.id'), 
399                 array(
400                     'container_id'            => 'container_id'
401                 )
402             );
403
404         return $select;
405     }
406     
407     /**
408      * set the password for given account
409      *
410      * @param   string  $_userId
411      * @param   string  $_password
412      * @param   bool    $_encrypt encrypt password
413      * @param   bool    $_mustChange
414      * @return  void
415      * @throws  Tinebase_Exception_InvalidArgument
416      */
417     public function setPassword($_userId, $_password, $_encrypt = TRUE, $_mustChange = null)
418     {
419         $userId = $_userId instanceof Tinebase_Model_User ? $_userId->getId() : $_userId;
420         $user = $_userId instanceof Tinebase_Model_FullUser ? $_userId : $this->getFullUserById($userId);
421         $this->checkPasswordPolicy($_password, $user);
422         
423         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
424         
425         $accountData['password'] = ($_encrypt) ? Hash_Password::generate('SSHA256', $_password) : $_password;
426         $accountData['last_password_change'] = Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG);
427         
428         $where = array(
429             $accountsTable->getAdapter()->quoteInto($accountsTable->getAdapter()->quoteIdentifier('id') . ' = ?', $userId)
430         );
431         
432         $result = $accountsTable->update($accountData, $where);
433         
434         if ($result != 1) {
435             throw new Tinebase_Exception_NotFound('Unable to update password! account not found in authentication backend.');
436         }
437         
438         $this->_setPluginsPassword($userId, $_password, $_encrypt);
439     }
440     
441     /**
442      * set password in plugins
443      * 
444      * @param string $userId
445      * @param string $password
446      * @param bool   $encrypt encrypt password
447      * @throws Tinebase_Exception_Backend
448      */
449     protected function _setPluginsPassword($userId, $password, $encrypt = TRUE)
450     {
451         foreach ($this->_sqlPlugins as $plugin) {
452             try {
453                 $plugin->inspectSetPassword($userId, $password, $encrypt);
454             } catch (Exception $e) {
455                 Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Could not change plugin password: ' . $e);
456                 throw new Tinebase_Exception_Backend($e->getMessage());
457             }
458         }
459     }
460     
461     /**
462      * ensure password policy
463      * 
464      * @param string $password
465      * @param Tinebase_Model_FullUser $user
466      * @throws Tinebase_Exception_PasswordPolicyViolation
467      */
468     public function checkPasswordPolicy($password, Tinebase_Model_FullUser $user)
469     {
470         if (! Tinebase_Config::getInstance()->get(Tinebase_Config::PASSWORD_POLICY_ACTIVE, false)) {
471             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
472                 . ' No password policy enabled');
473             return;
474         }
475         
476         $failedTests = array();
477         
478         $policy = array(
479             Tinebase_Config::PASSWORD_POLICY_ONLYASCII              => '/[^\x00-\x7F]/',
480             Tinebase_Config::PASSWORD_POLICY_MIN_LENGTH             => null,
481             Tinebase_Config::PASSWORD_POLICY_MIN_WORD_CHARS         => '/[\W]*/',
482             Tinebase_Config::PASSWORD_POLICY_MIN_UPPERCASE_CHARS    => '/[^A-Z]*/',
483             Tinebase_Config::PASSWORD_POLICY_MIN_SPECIAL_CHARS      => '/[\w]*/',
484             Tinebase_Config::PASSWORD_POLICY_MIN_NUMBERS            => '/[^0-9]*/',
485             Tinebase_Config::PASSWORD_POLICY_FORBID_USERNAME        => $user->accountLoginName,
486         );
487         
488         foreach ($policy as $key => $regex) {
489             $test = $this->_testPolicy($password, $key, $regex);
490             if ($test !== true) {
491                 $failedTests[$key] = $test;
492             }
493         }
494         
495         if (! empty($failedTests)) {
496             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
497                 . ' ' . print_r($failedTests, true));
498             
499             $policyException = new Tinebase_Exception_PasswordPolicyViolation('Password failed to match the following policy requirements: ' 
500                 . implode('|', array_keys($failedTests)));
501             throw $policyException;
502         }
503     }
504     
505     /**
506      * test password policy
507      * 
508      * @param string $password
509      * @param string $configKey
510      * @param string $regex
511      * @return mixed
512      */
513     protected function _testPolicy($password, $configKey, $regex = null)
514     {
515         $result = true;
516         
517         switch ($configKey) {
518             case Tinebase_Config::PASSWORD_POLICY_ONLYASCII:
519                 if (Tinebase_Config::getInstance()->get($configKey, 0) && $regex !== null) {
520                     $nonAsciiFound = preg_match($regex, $password, $matches);
521                     
522                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
523                         __METHOD__ . '::' . __LINE__ . ' ' . print_r($matches, true));
524                     
525                     $result = ($nonAsciiFound) ? array('expected' => 0, 'got' => count($matches)) : true;
526                 }
527                 
528                 break;
529                 
530             case Tinebase_Config::PASSWORD_POLICY_FORBID_USERNAME:
531                 if (Tinebase_Config::getInstance()->get($configKey, 0)) {
532                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
533                         __METHOD__ . '::' . __LINE__ . ' Testing if password is part of username "' . $regex . '"');
534                     
535                     if (! empty($password)) {
536                         $result = ! preg_match('/' . preg_quote($password) . '/i', $regex);
537                     }
538                 }
539                 
540                 break;
541                 
542             default:
543                 // check min length restriction
544                 $minLength = Tinebase_Config::getInstance()->get($configKey, 0);
545                 if ($minLength > 0) {
546                     $reduced = ($regex) ? preg_replace($regex, '', $password) : $password;
547                     $charCount = strlen(utf8_decode($reduced));
548                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
549                         . ' Found ' . $charCount . '/' . $minLength . ' chars for ' . $configKey /*. ': ' . $reduced */);
550                     
551                     if ($charCount < $minLength) {
552                         $result = array('expected' => $minLength, 'got' => $charCount);
553                     }
554                 }
555                 
556                 break;
557         }
558         
559         return $result;
560     }
561     
562     /**
563      * set the status of the user
564      *
565      * @param mixed   $_accountId
566      * @param string  $_status
567      * @return integer
568      * @throws Tinebase_Exception_InvalidArgument
569      */
570     public function setStatus($_accountId, $_status)
571     {
572         if ($this instanceof Tinebase_User_Interface_SyncAble) {
573             $this->setStatusInSyncBackend($_accountId, $_status);
574         }
575         
576         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
577         
578         switch($_status) {
579             case Tinebase_Model_User::ACCOUNT_STATUS_ENABLED:
580                 $accountData[$this->rowNameMapping['loginFailures']]  = 0;
581                 $accountData[$this->rowNameMapping['accountExpires']] = null;
582                 $accountData['status'] = $_status;
583                 break;
584                 
585             case Tinebase_Model_User::ACCOUNT_STATUS_DISABLED:
586                 $accountData['status'] = $_status;
587                 break;
588                 
589             case Tinebase_Model_User::ACCOUNT_STATUS_EXPIRED:
590                 $expiryDate = Tinebase_DateTime::now()->subSecond(1);
591                 $accountData['expires_at'] = $expiryDate->toString();
592                 if ($this instanceof Tinebase_User_Interface_SyncAble) {
593                     $this->setExpiryDateInSyncBackend($_accountId, $expiryDate);
594                 }
595
596                 break;
597             
598             default:
599                 throw new Tinebase_Exception_InvalidArgument('$_status can be only enabled, disabled or expired');
600                 break;
601         }
602         
603         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
604
605         $where = array(
606             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
607         );
608         
609         $result = $accountsTable->update($accountData, $where);
610         
611         return $result;
612     }
613
614     /**
615      * sets/unsets expiry date 
616      *
617      * @param     mixed      $_accountId
618      * @param     Tinebase_DateTime  $_expiryDate set to NULL to disable expirydate
619     */
620     public function setExpiryDate($_accountId, $_expiryDate)
621     {
622         if ($this instanceof Tinebase_User_Interface_SyncAble) {
623             $this->setExpiryDateInSyncBackend($_accountId, $_expiryDate);
624         }
625         
626         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
627         
628         if($_expiryDate instanceof DateTime) {
629             $accountData['expires_at'] = $_expiryDate->get(Tinebase_Record_Abstract::ISO8601LONG);
630         } else {
631             $accountData['expires_at'] = NULL;
632         }
633         
634         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
635
636         $where = array(
637             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
638         );
639         
640         $result = $accountsTable->update($accountData, $where);
641         
642         return $result;
643     }
644     
645     /**
646      * set last login failure in accounts table
647      * 
648      * @param string $_loginName
649      * @return Tinebase_Model_FullUser|null user if found
650      * @see Tinebase/User/Tinebase_User_Interface::setLastLoginFailure()
651      */
652     public function setLastLoginFailure($_loginName)
653     {
654         try {
655             $user = $this->getUserByPropertyFromSqlBackend('accountLoginName', $_loginName, 'Tinebase_Model_FullUser');
656         } catch (Tinebase_Exception_NotFound $tenf) {
657             // nothing todo => is no existing user
658             return null;
659         }
660         
661         $values = array(
662             'last_login_failure_at' => Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG),
663             'login_failures'        => new Zend_Db_Expr($this->_db->quoteIdentifier('login_failures') . ' + 1')
664         );
665         
666         $where = array(
667             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId())
668         );
669         
670         $this->_db->update(SQL_TABLE_PREFIX . 'accounts', $values, $where);
671
672         return $user;
673     }
674     
675     /**
676      * update the lastlogin time of user
677      *
678      * @param int $_accountId
679      * @param string $_ipAddress
680      * @return integer
681      */
682     public function setLoginTime($_accountId, $_ipAddress) 
683     {
684         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
685         
686         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
687         
688         $accountData['last_login_from'] = $_ipAddress;
689         $accountData['last_login']      = Tinebase_DateTime::now()->get(Tinebase_Record_Abstract::ISO8601LONG);
690         $accountData['login_failures']  = 0;
691         
692         $where = array(
693             $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
694         );
695         
696         $result = $accountsTable->update($accountData, $where);
697         
698         return $result;
699     }
700     
701     /**
702      * update contact data(first name, last name, ...) of user
703      * 
704      * @param Addressbook_Model_Contact $contact
705      */
706     public function updateContact(Addressbook_Model_Contact $_contact)
707     {
708         if($this instanceof Tinebase_User_Interface_SyncAble) {
709             $this->updateContactInSyncBackend($_contact);
710         }
711         
712         return $this->updateContactInSqlBackend($_contact);
713     }
714     
715     /**
716      * update contact data(first name, last name, ...) of user in local sql storage
717      * 
718      * @param Addressbook_Model_Contact $contact
719      * @return integer
720      * @throws Exception
721      */
722     public function updateContactInSqlBackend(Addressbook_Model_Contact $_contact)
723     {
724         $contactId = $_contact->getId();
725
726         $accountData = array(
727             $this->rowNameMapping['accountDisplayName']  => $_contact->n_fileas,
728             $this->rowNameMapping['accountFullName']     => $_contact->n_fn,
729             $this->rowNameMapping['accountFirstName']    => $_contact->n_given,
730             $this->rowNameMapping['accountLastName']     => $_contact->n_family,
731             $this->rowNameMapping['accountEmailAddress'] => $_contact->email
732         );
733         
734         try {
735             $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
736             
737             $where = array(
738                 $this->_db->quoteInto($this->_db->quoteIdentifier('contact_id') . ' = ?', $contactId)
739             );
740             return $accountsTable->update($accountData, $where);
741
742         } catch (Exception $e) {
743             Tinebase_TransactionManager::getInstance()->rollBack();
744             throw($e);
745         }
746     }
747     
748     /**
749      * updates an user
750      * 
751      * this function updates an user 
752      *
753      * @param Tinebase_Model_FullUser $_user
754      * @return Tinebase_Model_FullUser
755      */
756     public function updateUser(Tinebase_Model_FullUser $_user)
757     {
758         if($this instanceof Tinebase_User_Interface_SyncAble) {
759             $this->updateUserInSyncBackend($_user);
760         }
761         
762         $updatedUser = $this->updateUserInSqlBackend($_user);
763         $this->updatePluginUser($updatedUser, $_user);
764         
765         return $updatedUser;
766     }
767     
768     /**
769     * update data in plugins
770     *
771     * @param Tinebase_Model_FullUser $updatedUser
772     * @param Tinebase_Model_FullUser $newUserProperties
773     */
774     public function updatePluginUser($updatedUser, $newUserProperties)
775     {
776         foreach ($this->_sqlPlugins as $plugin) {
777             $plugin->inspectUpdateUser($updatedUser, $newUserProperties);
778         }
779     }
780     
781     /**
782      * updates an user
783      * 
784      * this function updates an user 
785      *
786      * @param Tinebase_Model_FullUser $_user
787      * @return Tinebase_Model_FullUser
788      * @throws 
789      */
790     public function updateUserInSqlBackend(Tinebase_Model_FullUser $_user)
791     {
792         if(! $_user->isValid()) {
793             throw new Tinebase_Exception_Record_Validation('Invalid user object. ' . print_r($_user->getValidationErrors(), TRUE));
794         }
795
796         $accountId = Tinebase_Model_User::convertUserIdToInt($_user);
797
798         $oldUser = $this->getFullUserById($accountId);
799         
800         if (empty($_user->contact_id)) {
801             $_user->visibility = 'hidden';
802             $_user->contact_id = null;
803         }
804         $accountData = $this->_recordToRawData($_user);
805         // don't update id
806         unset($accountData['id']);
807         
808         // ignore all other states (expired and blocked)
809         if ($_user->accountStatus == Tinebase_User::STATUS_ENABLED) {
810             $accountData[$this->rowNameMapping['accountStatus']] = $_user->accountStatus;
811             
812             if ($oldUser->accountStatus === Tinebase_User::STATUS_BLOCKED) {
813                 $accountData[$this->rowNameMapping['loginFailures']] = 0;
814             } elseif ($oldUser->accountStatus === Tinebase_User::STATUS_EXPIRED) {
815                 $accountData[$this->rowNameMapping['accountExpires']] = null;
816             }
817         } elseif ($_user->accountStatus == Tinebase_User::STATUS_DISABLED) {
818             $accountData[$this->rowNameMapping['accountStatus']] = $_user->accountStatus;
819         }
820         
821         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($accountData, true));
822
823         try {
824             $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
825             
826             $where = array(
827                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $accountId)
828             );
829             $accountsTable->update($accountData, $where);
830             
831         } catch (Exception $e) {
832             Tinebase_TransactionManager::getInstance()->rollBack();
833             throw($e);
834         }
835         
836         return $this->getUserById($accountId, 'Tinebase_Model_FullUser');
837     }
838     
839     /**
840      * add an user
841      * 
842      * @param   Tinebase_Model_FullUser  $_user
843      * @return  Tinebase_Model_FullUser
844      */
845     public function addUser(Tinebase_Model_FullUser $_user)
846     {
847         if ($this instanceof Tinebase_User_Interface_SyncAble) {
848             $userFromSyncBackend = $this->addUserToSyncBackend($_user);
849             if ($userFromSyncBackend !== NULL) {
850                 // set accountId for sql backend sql backend
851                 $_user->setId($userFromSyncBackend->getId());
852             }
853         }
854         
855         $addedUser = $this->addUserInSqlBackend($_user);
856         $this->addPluginUser($addedUser, $_user);
857         
858         return $addedUser;
859     }
860     
861     /**
862      * add data from/to plugins
863      * 
864      * @param Tinebase_Model_FullUser $addedUser
865      * @param Tinebase_Model_FullUser $newUserProperties
866      */
867     public function addPluginUser($addedUser, $newUserProperties)
868     {
869         foreach ($this->_sqlPlugins as $plugin) {
870             $plugin->inspectAddUser($addedUser, $newUserProperties);
871         }
872     }
873     
874     /**
875      * add an user
876      * 
877      * @todo fix $contactData['container_id'] = 1;
878      *
879      * @param   Tinebase_Model_FullUser  $_user
880      * @return  Tinebase_Model_FullUser
881      */
882     public function addUserInSqlBackend(Tinebase_Model_FullUser $_user)
883     {
884         $_user->isValid(TRUE);
885         
886         $accountsTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
887         
888         if(!isset($_user->accountId)) {
889             $userId = $_user->generateUID();
890             $_user->setId($userId);
891         }
892         
893         if (empty($_user->contact_id)) {
894             $_user->visibility = 'hidden';
895             $_user->contact_id = null;
896         }
897         
898         $accountData = $this->_recordToRawData($_user);
899         
900         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Adding user to SQL backend: ' . $_user->accountLoginName);
901         
902         $accountsTable->insert($accountData);
903             
904         return $this->getUserById($_user->getId(), 'Tinebase_Model_FullUser');
905     }
906     
907     /**
908      * converts record into raw data for adapter
909      *
910      * @param  Tinebase_Record_Abstract $_user
911      * @return array
912      */
913     protected function _recordToRawData($_user)
914     {
915         $accountData = array(
916             'id'                => $_user->accountId,
917             'login_name'        => $_user->accountLoginName,
918             'status'            => $_user->accountStatus,
919             'expires_at'        => ($_user->accountExpires instanceof DateTime ? $_user->accountExpires->get(Tinebase_Record_Abstract::ISO8601LONG) : NULL),
920             'primary_group_id'  => $_user->accountPrimaryGroup,
921             'home_dir'          => $_user->accountHomeDirectory,
922             'login_shell'       => $_user->accountLoginShell,
923             'openid'            => $_user->openid,
924             'visibility'        => $_user->visibility,
925             'contact_id'        => $_user->contact_id,
926             $this->rowNameMapping['accountDisplayName']  => $_user->accountDisplayName,
927             $this->rowNameMapping['accountFullName']     => $_user->accountFullName,
928             $this->rowNameMapping['accountFirstName']    => $_user->accountFirstName,
929             $this->rowNameMapping['accountLastName']     => $_user->accountLastName,
930             $this->rowNameMapping['accountEmailAddress'] => $_user->accountEmailAddress,
931             'created_by'            => $_user->created_by,
932             'creation_time'         => $_user->creation_time,
933             'last_modified_by'      => $_user->last_modified_by,
934             'last_modified_time'    => $_user->last_modified_time,
935             'is_deleted'            => $_user->is_deleted,
936             'deleted_time'          => $_user->deleted_time,
937             'deleted_by'            => $_user->deleted_by,
938             'seq'                   => $_user->seq,
939         );
940         
941         $unsetIfEmpty = array('seq', 'creation_time', 'created_by', 'last_modified_by', 'last_modified_time', 'is_deleted', 'deleted_time', 'deleted_by');
942         foreach ($unsetIfEmpty as $property) {
943             if (empty($accountData[$property])) {
944                 unset($accountData[$property]);
945             }
946         }
947         
948         return $accountData;
949     }
950     
951     /**
952      * delete a user
953      *
954      * @param  mixed  $_userId
955      */
956     public function deleteUser($_userId)
957     {
958         $deletedUser = $this->deleteUserInSqlBackend($_userId);
959         
960         if($this instanceof Tinebase_User_Interface_SyncAble) {
961             $this->deleteUserInSyncBackend($deletedUser);
962         }
963         
964         // update data from plugins
965         foreach ($this->_sqlPlugins as $plugin) {
966             $plugin->inspectDeleteUser($deletedUser);
967         }
968         
969     }
970     
971     /**
972      * delete a user
973      *
974      * @param  mixed  $_userId
975      * @return Tinebase_Model_FullUser  the delete user
976      */
977     public function deleteUserInSqlBackend($_userId)
978     {
979         if ($_userId instanceof Tinebase_Model_FullUser) {
980             $user = $_userId;
981         } else {
982             $user = $this->getFullUserById($_userId);
983         }
984
985         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
986             . ' Deleting user' . $user->accountLoginName);
987
988         $event = new Tinebase_Event_User_DeleteAccount(
989             Tinebase_Config::getInstance()->get(Tinebase_Config::ACCOUNT_DELETION_EVENTCONFIGURATION, new Tinebase_Config_Struct())->toArray()
990         );
991         $event->account = $user;
992         Tinebase_Event::fireEvent($event);
993         
994         $accountsTable          = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'accounts'));
995         $groupMembersTable      = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'group_members'));
996         $roleMembersTable       = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'role_accounts'));
997         
998         try {
999             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($this->_db);
1000             
1001             $where  = array(
1002                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_id') . ' = ?', $user->getId()),
1003             );
1004             $groupMembersTable->delete($where);
1005
1006             $where  = array(
1007                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_id')   . ' = ?', $user->getId()),
1008                 $this->_db->quoteInto($this->_db->quoteIdentifier('account_type') . ' = ?', Tinebase_Acl_Rights::ACCOUNT_TYPE_USER),
1009             );
1010             $roleMembersTable->delete($where);
1011             
1012             $where  = array(
1013                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $user->getId()),
1014             );
1015             $accountsTable->delete($where);
1016
1017             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1018         } catch (Exception $e) {
1019             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' error while deleting account ' . $e->__toString());
1020             Tinebase_TransactionManager::getInstance()->rollBack();
1021             throw($e);
1022         }
1023         
1024         return $user;
1025     }
1026     
1027     /**
1028      * delete users
1029      * 
1030      * @param array $_accountIds
1031      */
1032     public function deleteUsers(array $_accountIds)
1033     {
1034         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1035             . ' Deleting ' . count($_accountIds) .' users');
1036
1037         foreach ($_accountIds as $accountId) {
1038             $this->deleteUser($accountId);
1039         }
1040     }
1041     
1042     /**
1043      * Delete all users returned by {@see getUsers()} using {@see deleteUsers()}
1044      * 
1045      * @return void
1046      */
1047     public function deleteAllUsers()
1048     {
1049         // need to fetch FullUser because otherwise we would get only enabled accounts :/
1050         $users = $this->getUsers(NULL, NULL, 'ASC', NULL, NULL, 'Tinebase_Model_FullUser');
1051         
1052         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Deleting ' . count($users) .' users');
1053         foreach ( $users as $user ) {
1054             $this->deleteUser($user);
1055         }
1056     }
1057
1058     /**
1059      * Get multiple users
1060      *
1061      * fetch FullUser by default
1062      *
1063      * @param  string|array $_id Ids
1064      * @param  string  $_accountClass  type of model to return
1065      * @return Tinebase_Record_RecordSet of 'Tinebase_Model_User' or 'Tinebase_Model_FullUser'
1066      */
1067     public function getMultiple($_id, $_accountClass = 'Tinebase_Model_FullUser') 
1068     {
1069         if (empty($_id)) {
1070             return new Tinebase_Record_RecordSet($_accountClass);
1071         }
1072         
1073         $select = $this->_getUserSelectObject()
1074             ->where($this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.id') . ' in (?)', (array) $_id);
1075         
1076         $stmt = $this->_db->query($select);
1077         $queryResult = $stmt->fetchAll();
1078         
1079         $result = new Tinebase_Record_RecordSet($_accountClass, $queryResult, TRUE);
1080         
1081         return $result;
1082     }
1083
1084     /**
1085      * send deactivation email to user
1086      * 
1087      * @param mixed $accountId
1088      */
1089     public function sendDeactivationNotification($accountId)
1090     {
1091         if (! Tinebase_Config::getInstance()->get(Tinebase_Config::ACCOUNT_DEACTIVATION_NOTIFICATION)) {
1092             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
1093                 __METHOD__ . '::' . __LINE__ . ' Deactivation notification disabled.');
1094             return;
1095         }
1096         
1097         try {
1098             $user = $this->getFullUserById($accountId);
1099             
1100             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
1101                 __METHOD__ . '::' . __LINE__ . ' Send deactivation notification to user ' . $user->accountLoginName);
1102             
1103             $translate = Tinebase_Translation::getTranslation('Tinebase');
1104             
1105             $view = new Zend_View();
1106             $view->setScriptPath(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'views');
1107             
1108             $view->translate            = $translate;
1109             $view->accountLoginName     = $user->accountLoginName;
1110             // TODO add this?
1111             //$view->deactivationDate     = $user->deactivationDate;
1112             $view->tine20Url            = Tinebase_Core::getHostname();
1113             
1114             $messageBody = $view->render('deactivationNotification.php');
1115             $messageSubject = $translate->_('Your Tine 2.0 account has been deactivated');
1116             
1117             $recipient = Addressbook_Controller_Contact::getInstance()->getContactByUserId($user->getId(), /* $_ignoreACL = */ true);
1118             Tinebase_Notification::getInstance()->send(/* sender = */ null, array($recipient), $messageSubject, $messageBody);
1119             
1120         } catch (Exception $e) {
1121             Tinebase_Exception::log($e);
1122         }
1123     }
1124
1125
1126     /**
1127      * returns number of current not-disabled, non-system users
1128      * 
1129      * @return number
1130      */
1131     public function countNonSystemUsers()
1132     {
1133         $select = $select = $this->_db->select()
1134             ->from(SQL_TABLE_PREFIX . 'accounts', 'COUNT(id)')
1135             ->where($this->_db->quoteIdentifier('login_name') . " not in ('cronuser', 'calendarscheduling')")
1136             ->where($this->_db->quoteInto($this->_db->quoteIdentifier('status') . ' != ?', Tinebase_Model_User::ACCOUNT_STATUS_DISABLED));
1137
1138         $userCount = $this->_db->fetchOne($select);
1139         return $userCount;
1140     }
1141
1142     /**
1143      * fetch creation time of the first/oldest user
1144      *
1145      * @return Tinebase_DateTime
1146      */
1147     public function getFirstUserCreationTime()
1148     {
1149         $fallback = new Tinebase_DateTime('2014-12-01');
1150         if (! $this->_userTableHasModlogFields()) {
1151             return $fallback;
1152         }
1153
1154         $select = $select = $this->_db->select()
1155             ->from(SQL_TABLE_PREFIX . 'accounts', 'creation_time')
1156             ->where($this->_db->quoteIdentifier('login_name') . " not in ('cronuser', 'calendarscheduling')")
1157             ->where($this->_db->quoteIdentifier('creation_time') . " is not null")
1158             ->order('creation_time ASC')
1159             ->limit(1);
1160         $creationTime = $this->_db->fetchOne($select);
1161
1162         $result = (!empty($creationTime)) ? new Tinebase_DateTime($creationTime) : $fallback;
1163         return $result;
1164     }
1165
1166     /**
1167      * checks if use table already has modlog fields
1168      *
1169      * @return bool
1170      */
1171     protected function _userTableHasModlogFields()
1172     {
1173         $schema = Tinebase_Db_Table::getTableDescriptionFromCache($this->_db->table_prefix . $this->_tableName, $this->_db);
1174         return isset($schema['creation_time']);
1175     }
1176
1177     /**
1178      * fetch all user ids from accounts table: updating from an old version fails if the modlog fields don't exist
1179      *
1180      * @return array
1181      */
1182     public function getAllUserIdsFromSqlBackend()
1183     {
1184         $sqlbackend = new Tinebase_Backend_Sql(array(
1185             'modelName' => 'Tinebase_Model_FullUser',
1186             'tableName' => $this->_tableName,
1187         ));
1188
1189         $userIds = $sqlbackend->search(null, null, Tinebase_Backend_Sql_Abstract::IDCOL);
1190         return $userIds;
1191     }
1192 }