0013268: show user report (CLI)
[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
107         $userAgents = $this->_getUserAgentsByInterval($_user);
108         if (count($userAgents) > $numberOfAllowedUserAgents) {
109             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
110                 . ' More than ' . $numberOfAllowedUserAgents . ' different UserAgents? we don\'t trust you!');
111             $result = true;
112         }
113         return $result;
114     }
115
116     /**
117      * @param        $_user
118      * @param string $_unit
119      * @param int    $_interval
120      * @return array
121      * @throws Tinebase_Exception_Backend_Database
122      *
123      * TODO move to backend
124      */
125     protected function _getUserAgentsByInterval($_user, $_unit = 'HOUR', $_interval = 1, $_onlyInvalidLogins = true)
126     {
127         $db = $this->_backend->getAdapter();
128         $dbCommand = Tinebase_Backend_Sql_Command::factory($db);
129         $select = $db->select()
130             ->distinct(true)
131             ->from($this->_backend->getTablePrefix() . $this->_backend->getTableName(), 'user_agent')
132             ->where( $db->quoteIdentifier('account_id') . ' = ?', $_user->getId() )
133             ->where( $db->quoteIdentifier('li') . ' > NOW() - ' . $dbCommand->getInterval($_unit, $_interval))
134             ->limit(20);
135
136         if ($_onlyInvalidLogins) {
137             $select->where( $db->quoteIdentifier('result') . ' <> ?', Tinebase_Auth::SUCCESS, Zend_Db::PARAM_INT);
138         }
139
140         $stmt = $db->query($select);
141         $result = $stmt->fetchAll();
142         $stmt->closeCursor();
143
144         return $result;
145     }
146
147     /**
148      * @param     $user
149      * @param int $lastMonths
150      * @return array of user clients
151      */
152     public function getUserClients($user, $lastMonths = 1)
153     {
154         $userAgents = $this->_getUserAgentsByInterval($user, 'MONTH', $lastMonths, /* $_onlyInvalidLogins */ false);
155         $result = array();
156         foreach ($userAgents as $row) {
157             // TODO maybe _getUserAgentsByInterval could already do this ...
158             if (isset($row['user_agent']) && $row['user_agent']) {
159                 $result[] = $row['user_agent'];
160             }
161         }
162         return $result;
163     }
164
165     /**
166      * get previous access log entry
167      * 
168      * @param Tinebase_Model_AccessLog $accessLog
169      * @throws Tinebase_Exception_NotFound
170      * @return Tinebase_Model_AccessLog
171      */
172     public function getPreviousAccessLog(Tinebase_Model_AccessLog $accessLog)
173     {
174         $previousAccessLog = $this->search(
175             new Tinebase_Model_AccessLogFilter(array(
176                 array(
177                     'field'    => 'ip',
178                     'operator' => 'equals',
179                     'value'    => $accessLog->ip
180                 ),
181                 array(
182                     'field'    => 'account_id',
183                     'operator' => 'equals',
184                     'value'    => $accessLog->account_id
185                 ),
186                 array(
187                     'field'    => 'result',
188                     'operator' => 'equals',
189                     'value'    => Tinebase_Auth::SUCCESS
190                 ),
191                 array(
192                     'field'    => 'clienttype',
193                     'operator' => 'equals',
194                     'value'    => $accessLog->clienttype
195                 ),
196                 array(
197                     'field'    => 'user_agent',
198                     'operator' => 'equals',
199                     'value'    => $accessLog->user_agent
200                 ),
201                 array(
202                     'field'    => 'lo',
203                     'operator' => 'after',
204                     'value'    => Tinebase_DateTime::now()->subHour(2) // @todo use session timeout from config
205                 ),
206             )),
207             new Tinebase_Model_Pagination(array(
208                 'sort'  => 'li',
209                 'dir'   => 'DESC',
210                 'limit' => 1
211             ))
212         )->getFirstRecord();
213         
214         if (!$previousAccessLog) {
215             throw new Tinebase_Exception_NotFound('previous access log entry not found');
216         }
217         
218         return $previousAccessLog;
219     }
220     
221     /**
222      * add logout entry to the access log
223      *
224      * @param string $_sessionId the session id
225      * @param string $_ipAddress the ip address the user connects from
226      * @return null|Tinebase_Model_AccessLog
227      */
228     public function setLogout()
229     {
230         $sessionId = Tinebase_Core::getSessionId();
231
232         try {
233             if (Tinebase_Core::isRegistered(Tinebase_Core::USERACCESSLOG)) {
234                 $loginRecord = Tinebase_Core::get(Tinebase_Core::USERACCESSLOG);
235             } else {
236                 $loginRecord = $this->_backend->getByProperty($sessionId, 'sessionid');
237             }
238         } catch (Tinebase_Exception_NotFound $tenf) {
239             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
240                 . ' Could not find access log login record for session id ' . $sessionId);
241             return null;
242         }
243         
244         $loginRecord->lo = Tinebase_DateTime::now();
245         
246         // call update of backend direct to save overhead of $this->update()
247         return $this->_backend->update($loginRecord);
248     }
249
250     /**
251      * clear access log table
252      * - if $date param is omitted, the last 60 days of access log are kept, the rest will be removed
253      * 
254      * @param Tinebase_DateTime $date
255      * @return integer deleted rows
256      * 
257      * @todo use $this->deleteByFilter($_filter)? might be slow for huge access_logs
258      */
259     public function clearTable($date = NULL)
260     {
261         $date = ($date instanceof Tinebase_DateTime) ? $date : Tinebase_DateTime::now()->subDay(60);
262         
263         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
264             . ' Removing all access log entries before ' . $date->toString());
265         
266         $db = $this->_backend->getAdapter();
267         $where = array(
268             $db->quoteInto($db->quoteIdentifier('li') . ' < ?', $date->toString())
269         );
270         $deletedRows = $db->delete($this->_backend->getTablePrefix() . $this->_backend->getTableName(), $where);
271         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
272             . ' Removed ' . $deletedRows . ' rows.');
273         
274         return $deletedRows;
275     }
276
277
278     /**
279      * return accessLog instance
280      *
281      * @param string $loginName
282      * @param Zend_Auth_Result $authResult
283      * @param Zend_Controller_Request_Abstract $request
284      * @param string $clientIdString
285      * @return Tinebase_Model_AccessLog
286      */
287     public function getAccessLogEntry($loginName, Zend_Auth_Result $authResult, \Zend\Http\Request $request, $clientIdString)
288     {
289         if ($header = $request->getHeaders('USER-AGENT')) {
290             $userAgent = substr($header->getFieldValue(), 0, 255);
291         } else {
292             $userAgent = 'unknown';
293         }
294
295         $accessLog = new Tinebase_Model_AccessLog(array(
296             'ip'         => $request->getServer('REMOTE_ADDR'),
297             'li'         => Tinebase_DateTime::now(),
298             'result'     => $authResult->getCode(),
299             'clienttype' => $clientIdString,
300             'login_name' => $loginName ? $loginName : $authResult->getIdentity(),
301             'user_agent' => $userAgent
302         ), true);
303
304         return $accessLog;
305     }
306
307     /**
308      * set session id for current request in accesslog
309      *
310      * @param Tinebase_Model_AccessLog $accessLog
311      */
312     public function setSessionId(Tinebase_Model_AccessLog $accessLog)
313     {
314         if (in_array($accessLog->clienttype, array(Tinebase_Server_WebDAV::REQUEST_TYPE, ActiveSync_Server_Http::REQUEST_TYPE))) {
315             try {
316                 $previousAccessLog = Tinebase_AccessLog::getInstance()->getPreviousAccessLog($accessLog);
317                 $accessLog->merge($previousAccessLog);
318             } catch (Tinebase_Exception_NotFound $tenf) {
319                 // ignore
320             }
321         }
322
323         if (empty($accessLog->sessionid)) {
324             $accessLog->sessionid = Tinebase_Core::getSessionId();
325         } else {
326             Tinebase_Core::set(Tinebase_Core::SESSIONID, $accessLog->sessionid);
327         }
328     }
329 }