fixes previous session id handling for activesync and webdav
[tine20] / tine20 / Tinebase / AccessLog.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  */ 
10
11 /**
12  * this class provides functions to get, add and remove entries from/to the access log
13  * 
14  * @package     Tinebase
15  */
16 class Tinebase_AccessLog extends Tinebase_Controller_Record_Abstract
17 {
18     /**
19      * @var Tinebase_Backend_Sql
20      */
21     protected $_backend;
22     
23     /**
24      * holds the instance of the singleton
25      *
26      * @var Tinebase_AccessLog
27      */
28     private static $_instance = NULL;
29     
30     /**
31      * the constructor
32      *
33      */
34     private function __construct()
35     {
36         $this->_modelName = 'Tinebase_Model_AccessLog';
37         $this->_omitModLog = TRUE;
38         $this->_doContainerACLChecks = FALSE;
39         
40         $this->_backend = new Tinebase_Backend_Sql(array(
41             'modelName' => $this->_modelName, 
42             'tableName' => 'access_log',
43         ));
44     }
45     
46     /**
47      * the singleton pattern
48      *
49      * @return Tinebase_AccessLog
50      */
51     public static function getInstance() 
52     {
53         if (self::$_instance === NULL) {
54             self::$_instance = new Tinebase_AccessLog;
55         }
56         
57         return self::$_instance;
58     }
59
60     /**
61      * returns false if not blocked and number of failed logins if
62      *
63      * @param Tinebase_Model_FullUser  $_user
64      * @param Tinebase_Model_AccessLog $_accessLog
65      * @return bool|integer
66      */
67     public function isUserAgentBlocked(Tinebase_Model_FullUser $_user, Tinebase_Model_AccessLog $_accessLog)
68     {
69         if ($this->_tooManyUserAgents($_user)) {
70             return true;
71         }
72
73         $db = $this->_backend->getAdapter();
74         $dbCommand = Tinebase_Backend_Sql_Command::factory($db);
75         $select = $db->select()
76             ->from($this->_backend->getTablePrefix() . $this->_backend->getTableName(), new Zend_Db_Expr('count(*)'))
77             ->where( $db->quoteIdentifier('account_id') . ' = ?', $_user->getId() )
78             ->where( $db->quoteIdentifier('li') . ' > NOW() - ' . $dbCommand->getInterval('MINUTE', '1'))
79             ->where( $db->quoteIdentifier('result') . ' <> ?', Tinebase_Auth::SUCCESS, Zend_Db::PARAM_INT)
80             ->where( $db->quoteIdentifier('user_agent') . ' = ?', $_accessLog->user_agent);
81
82         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $select);
83
84         $stmt = $db->query($select);
85         $count = $stmt->fetchColumn();
86         $stmt->closeCursor();
87
88         if ($count > 0) {
89             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
90                 . ' UserAgent blocked. Login failures: ' . $count);
91             return true;
92         }
93         return false;
94     }
95
96     /**
97      * check if user connected with too many user agent during the last hour
98      *
99      * @param Tinebase_Model_FullUser  $_user
100      * @param int $numberOfAllowedUserAgents
101      * @return bool
102      */
103     protected function _tooManyUserAgents($_user, $numberOfAllowedUserAgents = 3)
104     {
105         $result = false;
106         $db = $this->_backend->getAdapter();
107         $dbCommand = Tinebase_Backend_Sql_Command::factory($db);
108         $select = $db->select()
109             ->distinct(true)
110             ->from($this->_backend->getTablePrefix() . $this->_backend->getTableName(), 'user_agent')
111             ->where( $db->quoteIdentifier('account_id') . ' = ?', $_user->getId() )
112             ->where( $db->quoteIdentifier('li') . ' > NOW() - ' . $dbCommand->getInterval('HOUR', '1'))
113             ->where( $db->quoteIdentifier('result') . ' <> ?', Tinebase_Auth::SUCCESS, Zend_Db::PARAM_INT)
114             ->limit(10);
115
116         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $select);
117
118         $stmt = $db->query($select);
119
120         if ($stmt->columnCount() > $numberOfAllowedUserAgents) {
121             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
122                 . ' More than ' . $numberOfAllowedUserAgents . ' different UserAgents? we don\'t trust you!');
123             $result = true;
124         }
125         $stmt->closeCursor();
126         return $result;
127     }
128
129     /**
130      * get previous access log entry
131      * 
132      * @param Tinebase_Model_AccessLog $accessLog
133      * @throws Tinebase_Exception_NotFound
134      * @return Tinebase_Model_AccessLog
135      */
136     public function getPreviousAccessLog(Tinebase_Model_AccessLog $accessLog)
137     {
138         $previousAccessLog = $this->search(
139             new Tinebase_Model_AccessLogFilter(array(
140                 array(
141                     'field'    => 'ip',
142                     'operator' => 'equals',
143                     'value'    => $accessLog->ip
144                 ),
145                 array(
146                     'field'    => 'account_id',
147                     'operator' => 'equals',
148                     'value'    => $accessLog->account_id
149                 ),
150                 array(
151                     'field'    => 'result',
152                     'operator' => 'equals',
153                     'value'    => Tinebase_Auth::SUCCESS
154                 ),
155                 array(
156                     'field'    => 'clienttype',
157                     'operator' => 'equals',
158                     'value'    => $accessLog->clienttype
159                 ),
160                 array(
161                     'field'    => 'user_agent',
162                     'operator' => 'equals',
163                     'value'    => $accessLog->user_agent
164                 ),
165                 array(
166                     'field'    => 'lo',
167                     'operator' => 'after',
168                     'value'    => Tinebase_DateTime::now()->subHour(2) // @todo use session timeout from config
169                 ),
170             )),
171             new Tinebase_Model_Pagination(array(
172                 'sort'  => 'li',
173                 'dir'   => 'DESC',
174                 'limit' => 1
175             ))
176         )->getFirstRecord();
177         
178         if (!$previousAccessLog) {
179             throw new Tinebase_Exception_NotFound('previous access log entry not found');
180         }
181         
182         return $previousAccessLog;
183     }
184     
185     /**
186      * add logout entry to the access log
187      *
188      * @param string $_sessionId the session id
189      * @param string $_ipAddress the ip address the user connects from
190      * @return null|Tinebase_Model_AccessLog
191      */
192     public function setLogout()
193     {
194         $sessionId = Tinebase_Core::getSessionId();
195
196         try {
197             $loginRecord = $this->_backend->getByProperty($sessionId, 'sessionid');
198         } catch (Tinebase_Exception_NotFound $tenf) {
199             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not find access log login record for session id ' . $_sessionId);
200             return null;
201         }
202         
203         $loginRecord->lo = Tinebase_DateTime::now();
204         
205         // call update of backend direct to save overhead of $this->update()
206         return $this->_backend->update($loginRecord);
207     }
208
209     /**
210      * clear access log table
211      * - if $date param is ommitted, the last 60 days of access log are kept, the rest will be removed
212      * 
213      * @param Tinebase_DateTime $date
214      * @return integer deleted rows
215      * 
216      * @todo use $this->deleteByFilter($_filter)? might be slow for huge access_logs
217      */
218     public function clearTable($date = NULL)
219     {
220         $date = ($date instanceof Tinebase_DateTime) ? $date : Tinebase_DateTime::now()->subDay(60);
221         
222         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
223             . ' Removing all access log entries before ' . $date->toString());
224         
225         $db = $this->_backend->getAdapter();
226         $where = array(
227             $db->quoteInto($db->quoteIdentifier('li') . ' < ?', $date->toString())
228         );
229         $deletedRows = $db->delete($this->_backend->getTablePrefix() . $this->_backend->getTableName(), $where);
230         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
231             . ' Removed ' . $deletedRows . ' rows.');
232         
233         return $deletedRows;
234     }
235
236
237     /**
238      * return accessLog instance
239      *
240      * @param string $loginName
241      * @param Zend_Auth_Result $authResult
242      * @param Zend_Controller_Request_Abstract $request
243      * @param string $clientIdString
244      * @return Tinebase_Model_AccessLog
245      */
246     public function getAccessLogEntry($loginName, Zend_Auth_Result $authResult, \Zend\Http\Request $request, $clientIdString)
247     {
248         if ($header = $request->getHeaders('USER-AGENT')) {
249             $userAgent = substr($header->getFieldValue(), 0, 255);
250         } else {
251             $userAgent = 'unknown';
252         }
253
254         $accessLog = new Tinebase_Model_AccessLog(array(
255             'ip'         => $request->getServer('REMOTE_ADDR'),
256             'li'         => Tinebase_DateTime::now(),
257             'result'     => $authResult->getCode(),
258             'clienttype' => $clientIdString,
259             'login_name' => $loginName ? $loginName : $authResult->getIdentity(),
260             'user_agent' => $userAgent
261         ), true);
262
263         return $accessLog;
264     }
265
266     /**
267      * set session id for current request in accesslog
268      *
269      * @param Tinebase_Model_AccessLog $accessLog
270      */
271     public function setSessionId(Tinebase_Model_AccessLog $accessLog)
272     {
273         if (in_array($accessLog->clienttype, array(Tinebase_Server_WebDAV::REQUEST_TYPE, ActiveSync_Server_Http::REQUEST_TYPE))) {
274             try {
275                 $previousAccessLog = Tinebase_AccessLog::getInstance()->getPreviousAccessLog($accessLog);
276                 $accessLog->sessionid = $previousAccessLog->sessionid;
277             } catch (Tinebase_Exception_NotFound $tenf) {
278                 // ignore
279             }
280         }
281
282         if (empty($accessLog->sessionid)) {
283             $accessLog->sessionid = Tinebase_Core::getSessionId();
284         } else {
285             Tinebase_Core::set(Tinebase_Core::SESSIONID, $accessLog->sessionid);
286         }
287     }
288 }