diff --git a/plugins/logs/README.md b/plugins/logs/README.md new file mode 100644 index 0000000..876a9b9 --- /dev/null +++ b/plugins/logs/README.md @@ -0,0 +1,62 @@ +# Logger plugin + +## Overview +The Logger plugin provides a modular, pluggable logging system for the application. +It logs user and system events to a MySQL table named `log`. + +## Installation +1. Copy the entire `logger` folder into your project's `plugins/` directory. +2. Ensure `"enabled": true` in `plugins/logger/plugin.json`. +3. On first initialization, the plugin will create the `log` table if it does not already exist. + +## Database Schema +The plugin defines the following table (auto-created): +```sql +CREATE TABLE IF NOT EXISTS `log` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `scope` SET('user','system') NOT NULL, + `message` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + CONSTRAINT `log_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; +``` + +## Hook API +Core must call: +```php +// After DB connect: +do_hook('logger.system_init', ['db' => $db]); +``` +The plugin listens on `logger.system_init`, runs auto-migration, then sets: +```php +$GLOBALS['logObject']; // instance of Log +$GLOBALS['user_IP']; // current user IP +``` + +Then in the code use: +```php +$logObject->insertLog($userId, 'Your message', 'user'); +$data = $logObject->readLog($userId, 'user', $offset, $limit, $filters); +``` + +## File Structure +``` +plugins/logger/ +├─ bootstrap.php # registers hook +├─ plugin.json # metadata & enabled flag +├─ README.md # this documentation +├─ models/ +│ ├─ Log.php # main Log class +│ └─ LoggerFactory.php# migration + factory +├─ helpers/ +│ └─ logs.php # user IP helper +└─ migrations/ + └─ create_log_table.sql +``` + +## Uninstall / Disable +- Set `"enabled": false` in `plugin.json` or delete the `plugins/logger/` folder. +- Core code will default to `NullLogger` and no logs will be written. diff --git a/plugins/logs/bootstrap.php b/plugins/logs/bootstrap.php new file mode 100644 index 0000000..80c4628 --- /dev/null +++ b/plugins/logs/bootstrap.php @@ -0,0 +1,31 @@ + + Logs + '; +}); diff --git a/plugins/logs/controllers/logs.php b/plugins/logs/controllers/logs.php new file mode 100644 index 0000000..309b10c --- /dev/null +++ b/plugins/logs/controllers/logs.php @@ -0,0 +1,117 @@ +hasRight($userId, 'superuser') || + $userObject->hasRight($userId, 'view app logs')); + +// Get current page for pagination +$currentPage = $_REQUEST['page_num'] ?? 1; +$currentPage = (int)$currentPage; + +// Get selected tab +$selected_tab = $_REQUEST['tab'] ?? 'user'; +if ($selected_tab === 'system' && !$has_system_access) { + $selected_tab = 'user'; +} + +// Set scope based on selected tab +$scope = ($selected_tab === 'system') ? 'system' : 'user'; + +// specify time range +include '../app/helpers/time_range.php'; + +// Prepare search filters +$filters = []; +if (isset($_REQUEST['from_time']) && !empty($_REQUEST['from_time'])) { + $filters['from_time'] = $_REQUEST['from_time']; +} +if (isset($_REQUEST['until_time']) && !empty($_REQUEST['until_time'])) { + $filters['until_time'] = $_REQUEST['until_time']; +} +if (isset($_REQUEST['message']) && !empty($_REQUEST['message'])) { + $filters['message'] = $_REQUEST['message']; +} +if ($scope === 'system' && isset($_REQUEST['id']) && !empty($_REQUEST['id'])) { + $filters['id'] = $_REQUEST['id']; +} + +// pagination variables +$items_per_page = 15; +$offset = ($currentPage - 1) * $items_per_page; + +// Build params for pagination +$params = ''; +if (!empty($_REQUEST['from_time'])) { + $params .= '&from_time=' . urlencode($_REQUEST['from_time']); +} +if (!empty($_REQUEST['until_time'])) { + $params .= '&until_time=' . urlencode($_REQUEST['until_time']); +} +if (!empty($_REQUEST['message'])) { + $params .= '&message=' . urlencode($_REQUEST['message']); +} +if (!empty($_REQUEST['id'])) { + $params .= '&id=' . urlencode($_REQUEST['id']); +} +if (isset($_REQUEST['tab'])) { + $params .= '&tab=' . urlencode($_REQUEST['tab']); +} + +// prepare the result +$search = $logObject->readLog($userId, $scope, $offset, $items_per_page, $filters); +$search_all = $logObject->readLog($userId, $scope, 0, 0, $filters); + +if (!empty($search)) { + // we get total items and number of pages + $item_count = count($search_all); + $totalPages = ceil($item_count / $items_per_page); + + $logs = array(); + $logs['records'] = array(); + + foreach ($search as $item) { + // when we show only user's logs, omit user_id column + if ($scope === 'user') { + $log_record = array( + // assign title to the field in the array record + 'time' => $item['time'], + 'log message' => $item['message'] + ); + } else { + $log_record = array( + // assign title to the field in the array record + 'userID' => $item['user_id'], + 'username' => $item['username'], + 'time' => $item['time'], + 'log message' => $item['message'] + ); + } + + // populate the result array + array_push($logs['records'], $log_record); + } +} + +$username = $userObject->getUserDetails($userId)[0]['username']; + +// Get any new feedback messages +include dirname(__FILE__, 4) . '/app/helpers/feedback.php'; + +// Display messages list +include PLUGIN_LOGS_PATH . 'views/logs.php'; diff --git a/plugins/logs/models/Log.php b/plugins/logs/models/Log.php new file mode 100644 index 0000000..43bc527 --- /dev/null +++ b/plugins/logs/models/Log.php @@ -0,0 +1,122 @@ +db = $database->getConnection(); + } + + /** + * Insert a log event into the database. + * + * @param int $userId The ID of the user associated with the log event. + * @param string $message The log message to insert. + * @param string $scope The scope of the log event (e.g., 'user', 'system'). Default is 'user'. + * + * @return bool|string True on success, or an error message on failure. + */ + public function insertLog($userId, $message, $scope = 'user') { + try { + $sql = 'INSERT INTO logs + (user_id, scope, message) + VALUES + (:user_id, :scope, :message)'; + + $query = $this->db->prepare($sql); + $query->execute([ + ':user_id' => $userId, + ':scope' => $scope, + ':message' => $message, + ]); + + return true; + + } catch (Exception $e) { + return $e->getMessage(); + } + } + + /** + * Retrieve log entries from the database. + * + * @param int $userId The ID of the user whose logs are being retrieved. + * @param string $scope The scope of the logs ('user' or 'system'). + * @param int $offset The offset for pagination. Default is 0. + * @param int $items_per_page The number of log entries to retrieve per page. Default is no limit. + * @param array $filters Optional array of filters (from_time, until_time, message, id) + * + * @return array An array of log entries. + */ + public function readLog($userId, $scope, $offset = 0, $items_per_page = '', $filters = []) { + $params = []; + $where_clauses = []; + + // Base query with user join + $base_sql = 'SELECT l.*, u.username + FROM logs l + LEFT JOIN users u ON l.user_id = u.id'; + + // Add scope condition + if ($scope === 'user') { + $where_clauses[] = 'l.user_id = :user_id'; + $params[':user_id'] = $userId; + } + + // Add time range filters if specified + if (!empty($filters['from_time'])) { + $where_clauses[] = 'l.time >= :from_time'; + $params[':from_time'] = $filters['from_time'] . ' 00:00:00'; + } + if (!empty($filters['until_time'])) { + $where_clauses[] = 'l.time <= :until_time'; + $params[':until_time'] = $filters['until_time'] . ' 23:59:59'; + } + + // Add message search if specified + if (!empty($filters['message'])) { + $where_clauses[] = 'l.message LIKE :message'; + $params[':message'] = '%' . $filters['message'] . '%'; + } + + // Add user ID search if specified + if (!empty($filters['id'])) { + $where_clauses[] = 'l.user_id = :search_user_id'; + $params[':search_user_id'] = $filters['id']; + } + + // Combine WHERE clauses + $sql = $base_sql; + if (!empty($where_clauses)) { + $sql .= ' WHERE ' . implode(' AND ', $where_clauses); + } + + // Add ordering + $sql .= ' ORDER BY l.time DESC'; + + // Add pagination + if ($items_per_page) { + $items_per_page = (int)$items_per_page; + $sql .= ' LIMIT ' . $offset . ',' . $items_per_page; + } + + $query = $this->db->prepare($sql); + $query->execute($params); + + return $query->fetchAll(PDO::FETCH_ASSOC); + } +} diff --git a/plugins/logs/models/LoggerFactory.php b/plugins/logs/models/LoggerFactory.php new file mode 100644 index 0000000..f27d672 --- /dev/null +++ b/plugins/logs/models/LoggerFactory.php @@ -0,0 +1,34 @@ +getConnection(); +// $migrationFile = __DIR__ . '/../migrations/create_log_table.sql'; +// if (file_exists($migrationFile)) { +// $sql = file_get_contents($migrationFile); +// $pdo->exec($sql); +// } + + // Load models and core IP helper + require_once __DIR__ . '/Log.php'; + require_once __DIR__ . '/../../../app/helpers/ip_helper.php'; + + // Instantiate logger and retrieve user IP + $logger = new \Log($db); + $userIP = getUserIP(); + + return [$logger, $userIP]; + } +} diff --git a/plugins/logs/plugin.json b/plugins/logs/plugin.json new file mode 100644 index 0000000..9900497 --- /dev/null +++ b/plugins/logs/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "Logger Plugin", + "version": "1.0.0", + "enabled": true, + "description": "Initializes logging system via LoggerFactory" +} diff --git a/plugins/logs/views/logs.php b/plugins/logs/views/logs.php new file mode 100644 index 0000000..0f69e32 --- /dev/null +++ b/plugins/logs/views/logs.php @@ -0,0 +1,109 @@ + + +
Username (id) | + +Time | +Log message | +
---|---|---|
= $row['userID'] ? '' . htmlspecialchars($row['username'] . " ({$row['userID']})") . '' : 'SYSTEM' ?> | + += date('d M Y H:i', strtotime($row['time'])) ?> | += htmlspecialchars($row['log message']) ?> | +