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