1<?php
2/**
3 * EGroupware admin - access- and session-log
4 *
5 * @link http://www.egroupware.org
6 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7 * @package admin
8 * @copyright (c) 2009-19 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10 */
11
12use EGroupware\Api;
13use EGroupware\Api\Etemplate;
14
15/**
16 * Show EGroupware access- and session-log
17 */
18class admin_accesslog
19{
20	/**
21	 * Which methods of this class can be called as menuation
22	 *
23	 * @var array
24	 */
25	public $public_functions = array(
26		'index' => true,
27		'sessions' => true,
28	);
29
30	/**
31	 * Our storage object
32	 *
33	 * @var Api\Storage\Base
34	 */
35	protected $so;
36
37	/**
38	 * Name of our table
39	 */
40	const TABLE = 'egw_access_log';
41	/**
42	 * Name of app the table is registered
43	 */
44	const APP = 'phpgwapi';
45
46	/**
47	 * Constructor
48	 *
49	 */
50	function __construct()
51	{
52		$this->so = new Api\Storage\Base(self::APP,self::TABLE,null,'',true);
53		$this->so->timestamps = array('li', 'lo', 'session_dla', 'notification_heartbeat');
54	}
55
56	/**
57	 * query rows for the nextmatch widget
58	 *
59	 * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter' and
60	 *	'session_list' true: all sessions, false: whole access-log, 'active': only sessions with session-status active (browser, no sync)
61	 * @param array &$rows returned rows/competitions
62	 * @param array &$readonlys eg. to disable buttons based on acl, not use here, maybe in a derived class
63	 * @return int total number of rows
64	 */
65	function get_rows($query,&$rows,&$readonlys)
66	{
67		$heartbeat_limit = Api\Session::heartbeat_limit();
68
69		if ($query['session_list'])	// filter active sessions
70		{
71			$query['col_filter']['lo'] = null;	// not logged out
72			$query['col_filter'][0] = 'session_dla > '.(int)(time() - $GLOBALS['egw_info']['server']['sessions_timeout']);
73			// for push via fallback (no native push) we use the heartbeat (constant polling of notification app)
74			if (Api\Json\Push::onlyFallback())
75			{
76				$active_query = "notification_heartbeat > $heartbeat_limit";
77			}
78			else
79			{
80				// for native push we ask the push-server who is active
81				$online = (array)Api\Json\Push::online();
82				$active_query = $GLOBALS['egw']->db->expression(self::TABLE, ['account_id' => $online]);
83			}
84			switch((string)$query['session_list'])
85			{
86				case 'active':	// remove status != 'active', eg. CalDAV/eSync
87					$query['col_filter'][1] = $active_query;
88					$query['col_filter'][3] = "session_php NOT LIKE '% %'";	// remove blocked, bad login, etc
89					break;
90				default:
91					$query['col_filter'][1] = "(notification_heartbeat IS NULL OR $active_query)";
92					break;
93			}
94			$query['col_filter'][2] = 'account_id>0';
95		}
96		$total = $this->so->get_rows($query,$rows,$readonlys);
97
98		$heartbeat_limit_user = Api\DateTime::server2user($heartbeat_limit, 'ts');
99
100		foreach($rows as &$row)
101		{
102			$row['sessionstatus'] = 'success';
103			if (isset($online) ?
104				// we still need to check notification_heartbeat to distinguish from non-interactive session like *DAV
105				isset($row['notification_heartbeat']) && in_array($row['account_id'], $online) :
106				$row['notification_heartbeat'] > $heartbeat_limit_user)
107			{
108				$row['sessionstatus'] = 'active';
109			}
110			if (stripos($row['session_php'],'blocked') !== false ||
111				stripos($row['session_php'],'bad login') !== false ||
112				strpos($row['session_php'],' ') !== false)
113			{
114				$row['sessionstatus'] = $row['session_php'];
115			}
116			if ($row['lo']) {
117				$row['total'] = ($row['lo'] - $row['li']) / 60;
118				$row['sessionstatus'] = 'logged out';
119			}
120			// eg. for bad login or password
121			if (!$row['account_id']) $row['alt_loginid'] = ($row['loginid']?$row['loginid']:lang('none'));
122
123			// do not allow to kill or select own session
124			if ($GLOBALS['egw']->session->sessionid_access_log == $row['sessionid'] && $query['session_list'])
125			{
126				$row['class'] .= ' rowNoDelete ';
127			}
128			// do not allow to delete access log off active sessions
129			if (!$row['lo'] && $row['session_dla'] > time()-$GLOBALS['egw_info']['server']['sessions_timeout'] &&
130				in_array($row['sessionstatus'], array('active', 'success')) && !$query['session_list'])
131			{
132				$row['class'] .= ' rowNoDelete ';
133			}
134			$row['sessionstatus'] = lang($row['sessionstatus']);
135			unset($row['session_php']);	// for security reasons, do NOT give real PHP sessionid to UI
136
137			$row['os_browser'] = Api\Header\UserAgent::osBrowser($row['user_agent']);
138		}
139		if ($query['session_list'])
140		{
141			$rows['no_total'] = $rows['no_lo'] = true;
142		}
143		$GLOBALS['egw_info']['flags']['app_header'] = lang('Admin').' - '.
144			($query['session_list'] ? lang('View sessions') : lang('View Access Log')).
145			($query['col_filter']['account_id'] ? ': '.Api\Accounts::username($query['col_filter']['account_id']) : '');
146
147		return $total;
148	}
149
150	/**
151	 * Display the access log or session list
152	 *
153	 * @param array $content =null
154	 * @param string $msg =''
155	 * @param boolean $sessions_list =false
156	 */
157	function index(array $content=null, $msg='', $sessions_list=false)
158	{
159
160		if (is_array($content)) $sessions_list = $content['nm']['session_list'];
161
162		// check if user has access to requested functionality
163		if ($GLOBALS['egw']->acl->check($sessions_list ? 'current_sessions' : 'access_log_acces',1,'admin'))
164		{
165			$GLOBALS['egw']->redirect_link('/index.php');
166		}
167
168		if(!isset($content))
169		{
170			$content['nm'] = array(
171				'get_rows'       =>	'admin.admin_accesslog.get_rows',	// I  method/callback to request the data for the rows eg. 'notes.bo.get_rows'
172				'no_filter'      => True,	// I  disable the 1. filter
173				'no_filter2'     => True,	// I  disable the 2. filter (params are the same as for filter)
174				'no_cat'         => True,	// I  disable the cat-selectbox
175				'header_left'    =>	false,	// I  template to show left of the range-value, left-aligned (optional)
176				'header_right'   =>	false,	// I  template to show right of the range-value, right-aligned (optional)
177				'never_hide'     => True,	// I  never hide the nextmatch-line if less then maxmatch entries
178				'lettersearch'   => false,	// I  show a lettersearch
179				'start'          =>	0,		// IO position in list
180				'order'          =>	'li',	// IO name of the column to sort after (optional for the sortheaders)
181				'sort'           =>	'DESC',	// IO direction of the sort: 'ASC' or 'DESC'
182				'default_cols'   => '!session_action',	// I  columns to use if there's no user or default pref (! as first char uses all but the named columns), default all columns
183				'csv_fields'     =>	false,	// I  false=disable csv export, true or unset=enable it with auto-detected fieldnames,
184								//or array with name=>label or name=>array('label'=>label,'type'=>type) pairs (type is a eT widget-type)
185				'actions'		=> $this->get_actions($sessions_list),
186				'placeholder_actions' => false,
187				'row_id'			=> 'sessionid',
188			);
189			if ((int)$_GET['account_id'])
190			{
191				$content['nm']['col_filter']['account_id'] = (int)$_GET['account_id'];
192			}
193			if ($sessions_list)
194			{
195				$content['nm']['order'] = 'session_dla';
196				$content['nm']['options-selectcols'] = array(
197					'lo' => false,
198					'total' => false,
199				);
200			}
201			$content['nm']['session_list'] = $sessions_list;
202		}
203		//error_log(__METHOD__. ' accesslog =>' . array2string($content['nm']['selected']));
204		if ($content['nm']['action'])
205		{
206			if ($content['nm']['select_all'])
207			{
208				// get the whole selection
209				$query = array(
210					'search' => $content['nm']['search'],
211					'col_filter' => $content['nm']['col_filter']
212				);
213
214				@set_time_limit(0);			// switch off the execution time limit, as it's for big selections to small
215				$query['num_rows'] = -1;	// all
216				$all = $readonlys = array();
217				$this->get_rows($query,$all,$readonlys);
218				$content['nm']['selected'] = array();
219				foreach($all as $session)
220				{
221					$content['nm']['selected'][] = $session[$content['nm']['row_id']];
222				}
223			}
224			if (!count($content['nm']['selected']) && !$content['nm']['select_all'])
225			{
226				$msg = lang('You need to select some entries first!');
227			}
228			else
229			{
230				$success = $failed = $action = $action_msg = null;
231				if ($this->action($content['nm']['action'],$content['nm']['selected']
232					,$success,$failed,$action_msg,$msg))
233				{ // In case of action success
234					switch ($action_msg)
235					{
236						case'deleted':
237							$msg = lang('%1 log entries deleted.',$success);
238							break;
239						case'killed':
240							$msg = lang('%1 sessions killed',$success);
241					}
242				}
243				elseif($failed) // In case of action failiure
244				{
245					switch ($action_msg)
246					{
247						case'deleted':
248							$msg = lang('Error deleting log entry!');
249							break;
250						case'killed':
251							$msg = lang('Permission denied!');
252					}
253				}
254			}
255		}
256
257		$content['msg'] = $msg;
258		$content['percent'] = 100.0 * $GLOBALS['egw']->db->query(
259			'SELECT ((SELECT COUNT(*) FROM '.self::TABLE.' WHERE lo != 0) / COUNT(*)) FROM '.self::TABLE,
260			__LINE__,__FILE__)->fetchColumn();
261
262		$tmpl = new Etemplate('admin.accesslog');
263		$tmpl->exec('admin.admin_accesslog.index', $content, array(), $readonlys, array(
264			'nm' => $content['nm'],
265		));
266	}
267
268	/**
269	 * Apply an action to multiple logs
270	 *
271	 * @param type $action
272	 * @param type $checked
273	 * @param type $use_all
274	 * @param type $success
275	 * @param int $failed
276	 * @param type $action_msg
277	 * @return type number of failed
278	 */
279	function action($action,$checked,&$success,&$failed,&$action_msg)
280	{
281		$success = $failed = 0;
282		//error_log(__METHOD__.'selected:' . array2string($checked). 'action:' . $action);
283		switch ($action)
284		{
285			case "delete":
286				$action_msg = "deleted";
287				$del_msg= $this->so->delete(array('sessionid' => $checked));
288				if ($checked && $del_msg)
289				{
290					$success = $del_msg;
291				}
292				else
293				{
294					$failed ++;
295				}
296				break;
297			case "kill":
298				$action_msg = "killed";
299				$sessionid = $checked;
300				if (($key = array_search($GLOBALS['egw']->session->sessionid_access_log, $sessionid)))
301				{
302						unset($sessionid[$key]);	// dont allow to kill own sessions
303				}
304				if ($GLOBALS['egw']->acl->check('current_sessions',8,'admin'))
305				{
306					$failed ++;
307				}
308				else
309				{
310					foreach((array)$sessionid as $id)
311					{
312						$GLOBALS['egw']->session->destroy($id);
313					}
314					$success= count($sessionid);
315				}
316				break;
317		}
318		return !$failed;
319	}
320
321	/**
322	 * Get actions / context menu for index
323	 *
324	 * Changes here, require to log out, as $content['nm'] get stored in session!
325	 *
326	 * @return array see nextmatch_widget::egw_actions()
327	 */
328	private static function get_actions($sessions_list)
329	{
330		$group = 0;
331		if ($sessions_list)
332		{
333		//	error_log(__METHOD__. $sessions_list);
334			$actions= array(
335				'kill' => array(
336					'caption' => 'Kill',
337					'confirm' => 'Kill this session',
338					'confirm_multiple' => 'Kill these sessions',
339					'group' => $group,
340					'disableClass' => 'rowNoDelete',
341				),
342			);
343
344		}
345		else
346		{
347			$actions= array(
348				'delete' => array(
349					'caption' => 'Delete',
350					'confirm' => 'Delete this entry',
351					'confirm_multiple' => 'Delete these entries',
352					'group' => $group,
353					'disableClass' => 'rowNoDelete',
354				),
355			);
356		}
357		// Automatic select all doesn't work with only 1 action
358		$actions['select_all'] = array(
359			'caption' => 'Select all',
360			//'checkbox' => true,
361			'hint' => 'Select all entries',
362			'enabled' => true,
363			'shortcut' => array(
364				'keyCode'	=>	65, // A
365				'ctrl'		=>	true,
366				'caption'	=> lang('Ctrl').'+A'
367			),
368			'group' => $group++,
369		);
370		return $actions;
371	}
372
373	/**
374	 * Display session list
375	 *
376	 * @param array $content =null
377	 * @param string $msg =''
378	 */
379	function sessions(array $content=null, $msg='')
380	{
381		return $this->index($content, $msg, true);
382	}
383}
384