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