Compare commits
7 Commits
0447439f99
...
e8576d3e94
Author | SHA1 | Date |
---|---|---|
|
e8576d3e94 | |
|
ff28ebf753 | |
|
242b63317b | |
|
a004602ce2 | |
|
c749726a79 | |
|
761c27c0d3 | |
|
fe91a91081 |
|
@ -1,126 +1,42 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* class Log
|
||||
*
|
||||
* Handles logging events into a database and reading log entries.
|
||||
* Log wrapper that delegates to plugin Log or NullLogger fallback.
|
||||
* Used when code does require_once '../app/classes/log.php'.
|
||||
*/
|
||||
|
||||
// If there is already a Log plugin loaded
|
||||
if (class_exists('Log')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load fallback NullLogger
|
||||
require_once __DIR__ . '/../core/NullLogger.php';
|
||||
|
||||
class Log {
|
||||
/**
|
||||
* @var PDO|null $db The database connection instance.
|
||||
*/
|
||||
private $db;
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Logs constructor.
|
||||
* Initializes the database connection.
|
||||
*
|
||||
* @param object $database The database object to initialize the connection.
|
||||
* @param mixed $database Database or DatabaseConnector instance
|
||||
*/
|
||||
public function __construct($database) {
|
||||
if ($database instanceof PDO) {
|
||||
$this->db = $database;
|
||||
global $logObject;
|
||||
if (isset($logObject) && method_exists($logObject, 'insertLog')) {
|
||||
$this->logger = $logObject;
|
||||
} else {
|
||||
$this->db = $database->getConnection();
|
||||
$this->logger = new \App\Core\NullLogger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a log event into the database.
|
||||
* Delegate insertLog to underlying logger
|
||||
*
|
||||
* @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.
|
||||
* @param mixed $userId
|
||||
* @param string $message
|
||||
* @param string|null $scope
|
||||
* @return mixed True on success or error message
|
||||
*/
|
||||
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);
|
||||
public function insertLog($userId, string $message, ?string $scope = null) {
|
||||
return $this->logger->insertLog($userId, $message, $scope);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,10 @@ class RateLimiter {
|
|||
} else {
|
||||
$this->db = $database->getConnection();
|
||||
}
|
||||
// Initialize logger via Log wrapper
|
||||
require_once __DIR__ . '/log.php';
|
||||
$this->log = new Log($database);
|
||||
// Initialize database tables
|
||||
$this->createTablesIfNotExist();
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@ class User {
|
|||
*/
|
||||
public function login($username, $password, $twoFactorCode = null) {
|
||||
// Get user's IP address
|
||||
require_once __DIR__ . '/../helpers/logs.php';
|
||||
$ipAddress = getUserIP();
|
||||
|
||||
// Check rate limiting first
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
/**
|
||||
* NullLogger is a fallback for disabling logging when there is no logging plugin enabled.
|
||||
*/
|
||||
class NullLogger
|
||||
{
|
||||
/**
|
||||
* No-op insertLog.
|
||||
*
|
||||
* @param mixed $userId
|
||||
* @param string $message
|
||||
* @param string|null $type
|
||||
* @return void
|
||||
*/
|
||||
public function insertLog($userId, string $message, ?string $type = null): void {}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Returns the user's IP address.
|
||||
* Uses global $user_IP set by Logger plugin if available, else falls back to server variables.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getUserIP() {
|
||||
global $user_IP;
|
||||
if (!empty($user_IP)) {
|
||||
return $user_IP;
|
||||
}
|
||||
// Fallback to HTTP headers or REMOTE_ADDR
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
return $_SERVER['HTTP_CLIENT_IP'];
|
||||
}
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
// May contain multiple IPs
|
||||
$parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($parts[0]);
|
||||
}
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Returns a logger instance: plugin Log if available, otherwise NullLogger.
|
||||
*
|
||||
* @param mixed $database Database or DatabaseConnector instance.
|
||||
* @return mixed Logger instance with insertLog() method.
|
||||
*/
|
||||
function getLoggerInstance($database) {
|
||||
if (class_exists('Log')) {
|
||||
return new Log($database);
|
||||
}
|
||||
require_once __DIR__ . '/../core/NullLogger.php';
|
||||
return new \App\Core\NullLogger();
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
function getUserIP() {
|
||||
// get directly the user IP
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
// if user is behind some proxy
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
|
||||
// get only the first IP if there are more
|
||||
if (strpos($ip, ',') !== false) {
|
||||
$ip = explode(',', $ip)[0];
|
||||
}
|
||||
|
||||
return trim($ip);
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/../helpers/security.php';
|
||||
require_once __DIR__ . '/../helpers/logs.php';
|
||||
|
||||
function applyCsrfMiddleware() {
|
||||
global $logObject;
|
||||
global $logObject, $user_IP;
|
||||
$security = SecurityHelper::getInstance();
|
||||
|
||||
// Skip CSRF check for GET requests
|
||||
|
@ -34,7 +33,7 @@ function applyCsrfMiddleware() {
|
|||
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!$security->verifyCsrfToken($token)) {
|
||||
// Log CSRF attempt
|
||||
$ipAddress = getUserIP();
|
||||
$ipAddress = $user_IP;
|
||||
$logMessage = sprintf(
|
||||
"CSRF attempt detected - IP: %s, Page: %s, User: %s",
|
||||
$ipAddress,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/../classes/ratelimiter.php';
|
||||
require_once __DIR__ . '/../helpers/logs.php';
|
||||
|
||||
/**
|
||||
* Rate limit middleware for page requests
|
||||
|
@ -13,10 +12,10 @@ require_once __DIR__ . '/../helpers/logs.php';
|
|||
* @return bool True if request is allowed, false if rate limited
|
||||
*/
|
||||
function checkRateLimit($database, $endpoint, $userId = null, $existingRateLimiter = null) {
|
||||
global $app_root;
|
||||
global $app_root, $user_IP;
|
||||
$isTest = defined('PHPUNIT_RUNNING');
|
||||
$rateLimiter = $existingRateLimiter ?? new RateLimiter($database);
|
||||
$ipAddress = getUserIP();
|
||||
$ipAddress = $user_IP;
|
||||
|
||||
// Check if request is allowed
|
||||
if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) {
|
||||
|
|
|
@ -14,7 +14,6 @@ require '../app/classes/api_response.php';
|
|||
|
||||
// Initialize required objects
|
||||
$userObject = new User($dbWeb);
|
||||
$logObject = new Log($dbWeb);
|
||||
$configObject = new Config();
|
||||
|
||||
// For AJAX requests
|
||||
|
|
|
@ -24,8 +24,8 @@ try {
|
|||
// Initialize RateLimiter
|
||||
require_once '../app/classes/ratelimiter.php';
|
||||
$rateLimiter = new RateLimiter($db);
|
||||
|
||||
// Get user IP
|
||||
require_once '../app/helpers/ip_helper.php';
|
||||
$user_IP = getUserIP();
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
|
||||
<!-- 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 Jilo monitoring 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><strong><?= htmlspecialchars($row['username']) ?> (<?= htmlspecialchars($row['userID']) ?>)</strong></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 -->
|
|
@ -79,9 +79,7 @@
|
|||
</a>
|
||||
<?php } ?>
|
||||
<?php if ($userObject->hasRight($userId, 'view app logs')) {?>
|
||||
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=logs">
|
||||
<i class="fas fa-list"></i>Logs
|
||||
</a>
|
||||
<?php do_hook('main_menu', ['app_root' => $app_root, 'section' => 'main', 'position' => 100]); ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -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>';
|
||||
});
|
|
@ -8,8 +8,13 @@
|
|||
* It supports pagination and filtering.
|
||||
*/
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// 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') ||
|
||||
|
@ -105,5 +110,8 @@ if (!empty($search)) {
|
|||
|
||||
$username = $userObject->getUserDetails($userId)[0]['username'];
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/logs.php';
|
||||
// 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 -->
|
|
@ -99,7 +99,6 @@ $allowed_urls = [
|
|||
'settings',
|
||||
'security',
|
||||
'status',
|
||||
'logs',
|
||||
'help',
|
||||
|
||||
'login',
|
||||
|
@ -135,18 +134,32 @@ $public_pages = filter_public_pages($public_pages);
|
|||
|
||||
// Dispatch routing and auth
|
||||
require_once __DIR__ . '/../app/core/Router.php';
|
||||
$currentUser = \App\Core\Router::checkAuth($config, $app_root, $public_pages, $page);
|
||||
use App\Core\Router;
|
||||
$currentUser = Router::checkAuth($config, $app_root, $public_pages, $page);
|
||||
|
||||
// connect to DB via DatabaseConnector
|
||||
require_once __DIR__ . '/../app/core/DatabaseConnector.php';
|
||||
use App\Core\DatabaseConnector;
|
||||
$dbWeb = DatabaseConnector::connect($config);
|
||||
|
||||
// start logging
|
||||
require '../app/classes/log.php';
|
||||
include '../app/helpers/logs.php';
|
||||
$logObject = new Log($dbWeb);
|
||||
$user_IP = getUserIP();
|
||||
// Logging: default to NullLogger, plugin can override
|
||||
require_once __DIR__ . '/../app/core/NullLogger.php';
|
||||
use App\Core\NullLogger;
|
||||
$logObject = new NullLogger();
|
||||
// Get the user IP
|
||||
require_once __DIR__ . '/../app/helpers/ip_helper.php';
|
||||
$user_IP = '';
|
||||
|
||||
// Plugin: initialize logging system plugin if available
|
||||
do_hook('logger.system_init', ['db' => $dbWeb]);
|
||||
|
||||
// Override defaults if plugin provided real logger
|
||||
if (isset($GLOBALS['logObject'])) {
|
||||
$logObject = $GLOBALS['logObject'];
|
||||
}
|
||||
if (isset($GLOBALS['user_IP'])) {
|
||||
$user_IP = $GLOBALS['user_IP'];
|
||||
}
|
||||
|
||||
// Initialize security middleware
|
||||
require_once '../app/includes/csrf_middleware.php';
|
||||
|
|
Loading…
Reference in New Issue