From e29eed9d2205a6f1eb10cf7662739169a30e39cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Laumaill=C3=A9?= Date: Wed, 26 Feb 2025 07:49:14 +0100 Subject: [PATCH] Added 'See logs' for each user in admin page (fix for #4593) --- pages/users.js.php | 62 +++++++++ pages/users.php | 8 +- sources/users.logs.datatable.php | 231 +++++++++++++++++++++++++++++++ 3 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 sources/users.logs.datatable.php diff --git a/pages/users.js.php b/pages/users.js.php index 9dbe736d2..8ee1cf435 100755 --- a/pages/users.js.php +++ b/pages/users.js.php @@ -1226,6 +1226,68 @@ function(data) { $('#admin_change_user_encryption_code_target_user').val($(this).data('id')); // --- + + } else if ($(this).data('action') === 'logs') { + $('#row-list, #row-folders').addClass('hidden'); + $('#row-logs').removeClass('hidden'); + $('#row-logs-title').text( + $(this).data('fullname') + ) + var userID = $(this).data('id'); + + //Launch the datatables pluggin + var oTableLogs = $('#table-logs').DataTable({ + 'destroy': true, + 'paging': true, + 'searching': true, + 'order': [ + [0, 'desc'] + ], + 'info': true, + 'processing': false, + 'serverSide': true, + 'responsive': false, + 'select': true, + 'stateSave': false, + 'retrieve': false, + 'autoWidth': false, + 'ajax': { + url: '/sources/users.logs.datatable.php', + data: function(d) { + d.userId = userID; + } + }, + 'language': { + 'url': '/includes/language/datatables.get('user-language'); ?>.txt' + }, + 'columns': [{ + className: 'dt-body-left' + }, + { + className: 'dt-body-left' + }, + { + className: 'dt-body-left' + } + ], + 'preDrawCallback': function() { + toastr.remove(); + toastr.info('get('in_progress'); ?> ... '); + }, + 'drawCallback': function() { + // Tooltips + $('.infotip').tooltip(); + + // Inform user + toastr.remove(); + toastr.success( + 'get('done'); ?>', + '', { + timeOut: 1000 + } + ); + }, + }); } else if ($(this).data('action') === 'access-rights') { $('#row-list, #row-logs').addClass('hidden'); diff --git a/pages/users.php b/pages/users.php index 334c96e34..abf25f068 100755 --- a/pages/users.php +++ b/pages/users.php @@ -387,12 +387,12 @@
- +
- - - + + + diff --git a/sources/users.logs.datatable.php b/sources/users.logs.datatable.php new file mode 100644 index 000000000..8830f6925 --- /dev/null +++ b/sources/users.logs.datatable.php @@ -0,0 +1,231 @@ +. + * + * Certain components of this file may be under different licenses. For + * details, see the `licenses` directory or individual file headers. + * --- + * @file users.logs.datatable.php + * @author Nils Laumaillé (nils@teampass.net) + * @copyright 2009-2024 Teampass.net + * @license GPL-3.0 + * @see https://www.teampass.net + */ + + +use TeampassClasses\SessionManager\SessionManager; +use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +use TeampassClasses\Language\Language; +use EZimuel\PHPSecureSession; +use TeampassClasses\PerformChecks\PerformChecks; +use TeampassClasses\ConfigManager\ConfigManager; +use TeampassClasses\NestedTree\NestedTree; +use voku\helper\AntiXSS; + +// Load functions +require_once 'main.functions.php'; + +// init +loadClasses('DB'); +$session = SessionManager::getSession(); +$request = SymfonyRequest::createFromGlobals(); +$lang = new Language($session->get('user-language') ?? 'english'); +$antiXss = new AntiXSS(); + +// Load config +$configManager = new ConfigManager(); +$SETTINGS = $configManager->getAllSettings(); + +// Do checks +// Instantiate the class with posted data +$checkUserAccess = new PerformChecks( + dataSanitizer( + [ + 'type' => $request->request->get('type', '') !== '' ? htmlspecialchars($request->request->get('type')) : '', + ], + [ + 'type' => 'trim|escape', + ], + ), + [ + 'user_id' => returnIfSet($session->get('user-id'), null), + 'user_key' => returnIfSet($session->get('key'), null), + ] +); +// Handle the case +echo $checkUserAccess->caseHandler(); +if ( + $checkUserAccess->userAccessPage('items') === false || + $checkUserAccess->checkSession() === false +) { + // Not allowed page + $session->set('system-error_code', ERR_NOT_ALLOWED); + include $SETTINGS['cpassman_dir'] . '/error.php'; + exit; +} + +// Define Timezone +date_default_timezone_set(isset($SETTINGS['timezone']) === true ? $SETTINGS['timezone'] : 'UTC'); + +// Set header properties +header('Content-type: text/html; charset=utf-8'); +header('Cache-Control: no-cache, no-store, must-revalidate'); + +// --------------------------------- // + +// Configure AntiXSS to keep double-quotes +$antiXss->removeEvilAttributes(['style', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'onmousemove', 'onkeydown', 'onkeyup', 'onkeypress', 'onchange', 'onblur', 'onfocus', 'onabort', 'onerror', 'onscroll']); +$antiXss->removeEvilHtmlTags(['script', 'iframe', 'embed', 'object', 'applet', 'link', 'style']); + +//Columns name +$aColumns = ['date', 'label', 'action']; +$aSortTypes = ['asc', 'desc']; +//init SQL variables +$sWhere = $sOrder = $sLimit = ''; +$params = $request->query->all(); + +/* BUILD QUERY */ +// Paging +$sLimit = ''; +if (isset($params['length']) && (int) $params['length'] !== -1) { + $start = filter_var($params['start'], FILTER_SANITIZE_NUMBER_INT); + $length = filter_var($params['length'], FILTER_SANITIZE_NUMBER_INT); + $sLimit = " LIMIT $start, $length"; +} + +// Ordering +$sOrder = ''; +$order = $params['order'][0] ?? null; +if ($order && in_array($order['dir'], $aSortTypes)) { + $sOrder = ' ORDER BY '; + if (isset($order['column']) && preg_match('#^(asc|desc)$#i', $order['dir'])) { + $columnIndex = filter_var($order['column'], FILTER_SANITIZE_NUMBER_INT); + $dir = filter_var($order['dir'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $sOrder .= $aColumns[$columnIndex] . ' ' . $dir . ', '; + } + + $sOrder = substr_replace($sOrder, '', -2); + if ($sOrder === ' ORDER BY') { + $sOrder = ''; + } +} + +/* + * Filtering + * NOTE this does not match the built-in DataTables filtering which does it + * word by word on any field. It's possible to do here, but concerned about efficiency + * on very large tables, and MySQL's regex functionality is very limited +*/ +$sWhere = ''; +$letter = $request->query->filter('letter', '', FILTER_SANITIZE_FULL_SPECIAL_CHARS); +$searchValue = isset($params['search']) && isset($params['search']['value']) ? filter_var($params['search']['value'], FILTER_SANITIZE_FULL_SPECIAL_CHARS) : ''; +if ($letter !== '' && $letter !== 'None') { + $sWhere = ' AND ('; + $sWhere .= $aColumns[1]." LIKE '".$letter."%'"; + $sWhere .= ')'; +} elseif ($searchValue !== '') { + $sWhere = ' AND ('; + $sWhere .= $aColumns[1]." LIKE '%".$searchValue."%'"; + $sWhere .= ')'; +} + +$rows = DB::query( + 'SELECT l.date as date, i.label as label, l.action as action + FROM '.prefixTable('log_items').' as l + INNER JOIN '.prefixTable('items').' as i ON (l.id_item=i.id) + INNER JOIN '.prefixTable('users').' as u ON (l.id_user=u.id) + WHERE u.id = '.$request->query->filter('userId', FILTER_SANITIZE_NUMBER_INT). + (string) $sWhere. + ' UNION '. + 'SELECT s.date AS date, s.label AS label, s.field_1 AS field1 + FROM '.prefixTable('log_system').' AS s + WHERE s.qui = '.$request->query->filter('userId', FILTER_SANITIZE_NUMBER_INT) +); +$iTotal = DB::count(); +$rows = DB::query( + 'SELECT l.date as date, i.label as label, l.action as action, i.id as id + FROM '.prefixTable('log_items').' as l + INNER JOIN '.prefixTable('items').' as i ON (l.id_item=i.id) + INNER JOIN '.prefixTable('users').' as u ON (l.id_user=u.id) + WHERE u.id = '.$request->query->filter('userId', FILTER_SANITIZE_NUMBER_INT). + (string) $sWhere. + ' UNION + SELECT s.date AS date, s.label AS label, s.field_1 AS field1, s.id as id + FROM '.prefixTable('log_system').' AS s + WHERE s.qui = '.$request->query->filter('userId', FILTER_SANITIZE_NUMBER_INT). + (string) $sOrder. + (string) $sLimit +); +$sOutput = '{'; +$sOutput .= '"sEcho": '.(int) $request->query->filter('draw', FILTER_SANITIZE_NUMBER_INT).', '; +$sOutput .= '"iTotalRecords": '.$iTotal.', '; +$sOutput .= '"iTotalDisplayRecords": '.$iTotal.', '; +$sOutput .= '"aaData": '; +if (DB::count() > 0) { + $sOutput .= '['; +} else { + $sOutput .= ''; +} + +foreach ($rows as $record) { + if (empty($record['action']) === true + || $record['action'] === $request->query->filter('userId', FILTER_SANITIZE_NUMBER_INT) + ) { + if (strpos($record['label'], 'at_') === 0) { + if (strpos($record['label'], '#') >= 0) { + $col2 = preg_replace('/#[\s\S]+?#/', '', $lang->get($record['label'])); + } else { + $col2 = str_replace('"', '\"', $lang->get($record['label'])); + } + } else { + $col2 = str_replace('"', '\"', $record['label']); + } + $col3 = ''; + } else { + $col2 = $lang->get($record['action']).' '.$lang->get('id').' '.$record['id']; + $col3 = str_replace('"', '\"', $record['label']); + } + + $sOutput .= '["'. + date($SETTINGS['date_format'].' '.$SETTINGS['time_format'], (int) $record['date']).'", '. + '"'.$col2.'", '. + '"'.$col3.'"],'; +} + +if (count($rows) > 0) { + if (strrchr($sOutput, '[') !== '[') { + $sOutput = substr_replace($sOutput, '', -1); + } + $sOutput .= ']'; +} else { + $sOutput .= '[]'; +} + + +if (count($rows) > 0) { + if (strrchr($sOutput, '[') !== '[') { + $sOutput = substr_replace($sOutput, '', -1); + } + $sOutput .= ']'; +} else { + $sOutput .= '[]'; +} + +echo ($sOutput).'}'; \ No newline at end of file
get('date'); ?>get('activity'); ?>get('label'); ?>get('date'); ?>get('activity'); ?>get('label'); ?>