Adds Log plugin
							parent
							
								
									fe91a91081
								
							
						
					
					
						commit
						761c27c0d3
					
				|  | @ -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. | ||||
|  | @ -0,0 +1,31 @@ | |||
| <?php | ||||
| 
 | ||||
| // Logger plugin bootstrap
 | ||||
| register_hook('logger.system_init', function(array $context) { | ||||
|     // Load plugin-specific LoggerFactory class
 | ||||
|     require_once __DIR__ . '/models/LoggerFactory.php'; | ||||
|     [$logger, $userIP] = LoggerFactory::create($context['db']); | ||||
| 
 | ||||
|     // Expose to globals for routing logic
 | ||||
|     $GLOBALS['logObject'] = $logger; | ||||
|     $GLOBALS['user_IP']   = $userIP; | ||||
| }); | ||||
| 
 | ||||
| // Add to allowed URLs
 | ||||
| register_hook('filter_allowed_urls', function($urls) { | ||||
|     $urls[] = 'logs'; | ||||
|     return $urls; | ||||
| }); | ||||
| 
 | ||||
| // Configuration for top menu injection
 | ||||
| define('LOGS_MAIN_MENU_SECTION', 'main'); // section of the top menu
 | ||||
| define('LOGS_MAIN_MENU_POSITION', 20);    // lower = earlier in menu
 | ||||
| register_hook('main_menu', function($ctx) { | ||||
|     $section = defined('LOGS_MAIN_MENU_SECTION') ? LOGS_MAIN_MENU_SECTION : 'main'; | ||||
|     $position = defined('LOGS_MAIN_MENU_POSITION') ? LOGS_MAIN_MENU_POSITION : 100; | ||||
|     // We use $section/$position for sorting/insertion logic in the menu template
 | ||||
|     echo ' | ||||
|                         <a class="dropdown-item" href="?page=logs"> | ||||
|                             <i class="fas fa-list"></i>Logs | ||||
|                         </a>'; | ||||
| }); | ||||
|  | @ -0,0 +1,117 @@ | |||
| <?php | ||||
| 
 | ||||
| /** | ||||
|  * Logs listings | ||||
|  * | ||||
|  * This page ("logs") retrieves and displays logs within a time range | ||||
|  * either for a specified user or for all users. | ||||
|  * It supports pagination and filtering. | ||||
|  */ | ||||
| 
 | ||||
| // Define plugin base path if not already defined
 | ||||
| if (!defined('PLUGIN_LOGS_PATH')) { | ||||
|     define('PLUGIN_LOGS_PATH', dirname(__FILE__, 2) . '/'); | ||||
| } | ||||
| require_once PLUGIN_LOGS_PATH . 'models/Log.php'; | ||||
| require_once PLUGIN_LOGS_PATH . 'models/LoggerFactory.php'; | ||||
| require_once dirname(__FILE__, 4) . '/app/classes/user.php'; | ||||
| 
 | ||||
| // Check for rights; user or system
 | ||||
| $has_system_access = ($userObject->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'; | ||||
|  | @ -0,0 +1,122 @@ | |||
| <?php | ||||
| 
 | ||||
| /** | ||||
|  * class Log | ||||
|  * | ||||
|  * Handles logging events into a database and reading log entries. | ||||
|  */ | ||||
| class Log { | ||||
|     /** | ||||
|      * @var PDO|null $db The database connection instance. | ||||
|      */ | ||||
|     private $db; | ||||
| 
 | ||||
|     /** | ||||
|      * Logs constructor. | ||||
|      * Initializes the database connection. | ||||
|      * | ||||
|      * @param object $database The database object to initialize the connection. | ||||
|      */ | ||||
|     public function __construct($database) { | ||||
|         $this->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); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,34 @@ | |||
| <?php | ||||
| 
 | ||||
| /** | ||||
|  * LoggerFactory for Logger Plugin. | ||||
|  * | ||||
|  * Responsible for auto-migration and creating the Log instance. | ||||
|  */ | ||||
| class LoggerFactory | ||||
| { | ||||
|     /** | ||||
|      * @param object $db Database connector instance. | ||||
|      * @return array [Log $logger, string $userIP] | ||||
|      */ | ||||
|     public static function create($db): array | ||||
|     { | ||||
|         // Auto-migration: ensure log table exists
 | ||||
|         $pdo = $db->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]; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,6 @@ | |||
| { | ||||
|   "name": "Logger Plugin", | ||||
|   "version": "1.0.0", | ||||
|   "enabled": true, | ||||
|   "description": "Initializes logging system via LoggerFactory" | ||||
| } | ||||
|  | @ -0,0 +1,109 @@ | |||
| 
 | ||||
|         <!-- log events --> | ||||
|         <div class="container-fluid mt-4"> | ||||
|             <div class="row mb-4"> | ||||
|                 <div class="col-md-6"> | ||||
|                     <h2 class="mb-0">Log events</h2> | ||||
|                     <small>events recorded in the platform</small> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- Tabs navigation --> | ||||
|             <ul class="nav nav-tabs mb-4"> | ||||
|                 <li class="nav-item"> | ||||
|                     <a class="nav-link <?= $scope === 'user' ? 'active' : '' ?>" href="?page=logs&tab=user"> | ||||
|                         Logs for current user | ||||
|                     </a> | ||||
|                 </li> | ||||
| <?php if ($has_system_access) { ?>
 | ||||
|                 <li class="nav-item"> | ||||
|                     <a class="nav-link <?= $scope === 'system' ? 'active' : '' ?>" href="?page=logs&tab=system"> | ||||
|                         Logs for all users | ||||
|                     </a> | ||||
|                 </li> | ||||
| <?php } ?>
 | ||||
|             </ul> | ||||
| 
 | ||||
|             <!-- logs filter --> | ||||
|             <div class="card mb-3"> | ||||
|                 <div class="card-body"> | ||||
|                     <form method="GET" action="" class="row g-3 align-items-end"> | ||||
|                         <input type="hidden" name="page" value="logs"> | ||||
|                         <input type="hidden" name="tab" value="<?= htmlspecialchars($scope) ?>"> | ||||
| 
 | ||||
|                         <div class="col-md-3"> | ||||
|                             <label for="from_time" class="form-label">From date</label> | ||||
|                             <input type="date" class="form-control" id="from_time" name="from_time" value="<?= htmlspecialchars($_REQUEST['from_time'] ?? '') ?>"> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div class="col-md-3"> | ||||
|                             <label for="until_time" class="form-label">Until date</label> | ||||
|                             <input type="date" class="form-control" id="until_time" name="until_time" value="<?= htmlspecialchars($_REQUEST['until_time'] ?? '') ?>"> | ||||
|                         </div> | ||||
| 
 | ||||
| <?php if ($scope === 'system') { ?>
 | ||||
|                         <div class="col-md-2"> | ||||
|                             <label for="id" class="form-label">User ID</label> | ||||
|                             <input type="text" class="form-control" id="id" name="id" value="<?= htmlspecialchars($_REQUEST['id'] ?? '') ?>" placeholder="Enter user ID"> | ||||
|                         </div> | ||||
| <?php } ?>
 | ||||
| 
 | ||||
|                         <div class="col-md"> | ||||
|                             <label for="message" class="form-label">Message</label> | ||||
|                             <input type="text" class="form-control" id="message" name="message" value="<?= htmlspecialchars($_REQUEST['message'] ?? '') ?>" placeholder="Search in log messages"> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div class="col-md-auto"> | ||||
|                             <button type="submit" class="btn btn-primary me-2"> | ||||
|                                 <i class="fas fa-search me-2"></i>Search | ||||
|                             </button> | ||||
|                             <a href="?page=logs&tab=<?= htmlspecialchars($scope) ?>" class="btn btn-outline-secondary"> | ||||
|                                 <i class="fas fa-times me-2"></i>Clear | ||||
|                             </a> | ||||
|                         </div> | ||||
|                     </form> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <!-- /logs filter --> | ||||
| 
 | ||||
|             <!-- logs --> | ||||
| <?php if ($time_range_specified) { ?>
 | ||||
|             <div class="alert alert-info m-3"> | ||||
|                 <i class="fas fa-calendar-alt me-2"></i>Time period: <strong><?= htmlspecialchars($from_time) ?> - <?= htmlspecialchars($until_time) ?></strong>
 | ||||
|             </div> | ||||
| <?php } ?>
 | ||||
|             <div class="mb-5"> | ||||
| <?php if (!empty($logs['records'])) { ?>
 | ||||
|                 <div class="table-responsive"> | ||||
|                     <table class="table table-hover align-middle mb-0"> | ||||
|                         <thead class="table-light"> | ||||
|                             <tr> | ||||
| <?php if ($scope === 'system') { ?>
 | ||||
|                                 <th>Username (id)</th> | ||||
| <?php } ?>
 | ||||
|                                 <th>Time</th> | ||||
|                                 <th>Log message</th> | ||||
|                             </tr> | ||||
|                         </thead> | ||||
|                         <tbody> | ||||
| <?php     foreach ($logs['records'] as $row) { ?>
 | ||||
|                             <tr> | ||||
| <?php         if ($scope === 'system') { ?>
 | ||||
|                                 <td><?= $row['userID'] ? '<strong>' . htmlspecialchars($row['username'] . " ({$row['userID']})") . '</strong>' : '<span class="text-muted font-weight-normal small">SYSTEM</span>' ?></td>
 | ||||
| <?php         } ?>
 | ||||
|                                 <td><span class="text-muted"><?= date('d M Y H:i', strtotime($row['time'])) ?></span></td>
 | ||||
|                                 <td><?= htmlspecialchars($row['log message']) ?></td>
 | ||||
|                             </tr> | ||||
| <?php     } ?>
 | ||||
|                         </tbody> | ||||
|                     </table> | ||||
|                 </div> | ||||
| <?php include '../app/templates/pagination.php'; ?>
 | ||||
| <?php } else { ?>
 | ||||
|                 <div class="alert alert-info m-3"> | ||||
|                     <i class="fas fa-info-circle me-2"></i>No log entries found for the specified criteria. | ||||
|                 </div> | ||||
| <?php } ?>
 | ||||
|             </div> | ||||
|         </div> | ||||
|         <!-- /log events --> | ||||
		Loading…
	
		Reference in New Issue