Compare commits

..

No commits in common. "main" and "v0.4" have entirely different histories.
main ... v0.4

110 changed files with 1561 additions and 4732 deletions

View File

@ -10,103 +10,21 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
services:
mariadb:
image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: jilo_test
MARIADB_USER: test_jilo
MARIADB_PASSWORD: test_password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h127.0.0.1 -P3306 -uroot -proot"
--health-interval=10s
--health-timeout=10s
--health-retries=5
--health-start-period=30s
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: '8.2' php-version: '8.1'
extensions: pdo, pdo_mysql, xdebug extensions: pdo, pdo_sqlite
coverage: xdebug
- name: Wait for MariaDB
run: |
sudo apt-get install -y mariadb-client
while ! mysqladmin ping -h"127.0.0.1" -P"3306" -uroot -proot --silent; do
echo "Waiting for database connection..."
sleep 2
done
- name: Install dependencies - name: Install dependencies
run: | run: |
cd tests cd tests
composer install composer install
- name: Test database connection
run: |
mysql -h127.0.0.1 -P3306 -uroot -proot -e "SHOW DATABASES;"
mysql -h127.0.0.1 -P3306 -uroot -proot -e "SELECT User,Host FROM mysql.user;"
mysql -h127.0.0.1 -P3306 -uroot -proot -e "GRANT ALL PRIVILEGES ON jilo_test.* TO 'test_jilo'@'%';"
mysql -h127.0.0.1 -P3306 -uroot -proot -e "FLUSH PRIVILEGES;"
- name: Update database config for CI
run: |
# Create temporary test config
mkdir -p tests/config
cat > tests/config/ci-config.php << 'EOF'
<?php
define('CI_DB_PASSWORD', 'test_password');
define('CI_DB_HOST', '127.0.0.1');
EOF
# Verify config file was created
echo "Config file contents:"
cat tests/config/ci-config.php
echo "\nConfig file location:"
ls -la tests/config/ci-config.php
# Grant access from Docker network
mysql -h127.0.0.1 -P3306 -uroot -proot -e "
DROP USER IF EXISTS 'test_jilo'@'%';
CREATE USER 'test_jilo'@'%' IDENTIFIED BY 'test_password';
GRANT ALL PRIVILEGES ON jilo_test.* TO 'test_jilo'@'%';
CREATE DATABASE IF NOT EXISTS jilo_test;
FLUSH PRIVILEGES;
"
# Update test files to require the config (using absolute path)
CONFIG_PATH=$(realpath tests/config/ci-config.php)
echo "\nConfig path: $CONFIG_PATH"
# Add require statement at the very start
for file in tests/Unit/Classes/{DatabaseTest,UserTest}.php; do
echo "<?php" > "$file.tmp"
echo "require_once '$CONFIG_PATH';" >> "$file.tmp"
tail -n +2 "$file" >> "$file.tmp"
mv "$file.tmp" "$file"
echo "\nFirst 5 lines of $file:"
head -n 5 "$file"
done
# Test database connection directly
echo "\nTesting database connection:"
mysql -h127.0.0.1 -P3306 -utest_jilo -ptest_password -e "SELECT 'Database connection successful!'" || echo "Database connection failed"
- name: Run test suite - name: Run test suite
run: | run: |
cd tests cd tests
./vendor/bin/phpunit ./vendor/bin/phpunit
# FIXME
# XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html coverage
# env:
# COMPOSER_PROCESS_TIMEOUT: 0
# COMPOSER_NO_INTERACTION: 1
# COMPOSER_NO_AUDIT: 1

View File

@ -13,8 +13,6 @@ All notable changes to this project will be documented in this file.
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.4...HEAD - gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.4...HEAD
### Added ### Added
- Added CSS and JS to the default theme
- Added change theme menu entry
### Changed ### Changed

View File

@ -43,11 +43,11 @@ class Agent {
jat.endpoint AS agent_endpoint, jat.endpoint AS agent_endpoint,
h.platform_id h.platform_id
FROM FROM
jilo_agent ja jilo_agents ja
JOIN JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN JOIN
host h ON ja.host_id = h.id hosts h ON ja.host_id = h.id
WHERE WHERE
ja.host_id = :host_id'; ja.host_id = :host_id';
@ -87,11 +87,11 @@ class Agent {
jat.endpoint AS agent_endpoint, jat.endpoint AS agent_endpoint,
h.platform_id h.platform_id
FROM FROM
jilo_agent ja jilo_agents ja
JOIN JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN JOIN
host h ON ja.host_id = h.id hosts h ON ja.host_id = h.id
WHERE WHERE
ja.id = :agent_id'; ja.id = :agent_id';
@ -110,7 +110,7 @@ class Agent {
*/ */
public function getAgentTypes() { public function getAgentTypes() {
$sql = 'SELECT * $sql = 'SELECT *
FROM jilo_agent_type FROM jilo_agent_types
ORDER BY id'; ORDER BY id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute(); $query->execute();
@ -131,7 +131,7 @@ class Agent {
id, id,
agent_type_id agent_type_id
FROM FROM
jilo_agent jilo_agents
WHERE WHERE
host_id = :host_id'; host_id = :host_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
@ -152,7 +152,7 @@ class Agent {
*/ */
public function addAgent($host_id, $newAgent) { public function addAgent($host_id, $newAgent) {
try { try {
$sql = 'INSERT INTO jilo_agent $sql = 'INSERT INTO jilo_agents
(host_id, agent_type_id, url, secret_key, check_period) (host_id, agent_type_id, url, secret_key, check_period)
VALUES VALUES
(:host_id, :agent_type_id, :url, :secret_key, :check_period)'; (:host_id, :agent_type_id, :url, :secret_key, :check_period)';
@ -184,7 +184,7 @@ class Agent {
*/ */
public function editAgent($agent_id, $updatedAgent) { public function editAgent($agent_id, $updatedAgent) {
try { try {
$sql = 'UPDATE jilo_agent $sql = 'UPDATE jilo_agents
SET SET
agent_type_id = :agent_type_id, agent_type_id = :agent_type_id,
url = :url, url = :url,
@ -222,7 +222,7 @@ class Agent {
*/ */
public function deleteAgent($agent_id) { public function deleteAgent($agent_id) {
try { try {
$sql = 'DELETE FROM jilo_agent $sql = 'DELETE FROM jilo_agents
WHERE WHERE
id = :agent_id'; id = :agent_id';
@ -420,13 +420,13 @@ class Agent {
jac.agent_id, jac.agent_id,
jat.description jat.description
FROM FROM
jilo_agent_check jac jilo_agent_checks jac
JOIN JOIN
jilo_agent ja ON jac.agent_id = ja.id jilo_agents ja ON jac.agent_id = ja.id
JOIN JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN JOIN
host h ON ja.host_id = h.id hosts h ON ja.host_id = h.id
WHERE WHERE
h.id = :host_id h.id = :host_id
AND jat.description = :agent_type AND jat.description = :agent_type
@ -520,13 +520,13 @@ class Agent {
jac.response_content, jac.response_content,
COUNT(*) as checks_count COUNT(*) as checks_count
FROM FROM
jilo_agent_check jac jilo_agent_checks jac
JOIN JOIN
jilo_agent ja ON jac.agent_id = ja.id jilo_agents ja ON jac.agent_id = ja.id
JOIN JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN JOIN
host h ON ja.host_id = h.id hosts h ON ja.host_id = h.id
WHERE WHERE
h.id = :host_id h.id = :host_id
AND jat.description = :agent_type AND jat.description = :agent_type
@ -591,13 +591,13 @@ class Agent {
jac.timestamp, jac.timestamp,
jac.response_content jac.response_content
FROM FROM
jilo_agent_check jac jilo_agent_checks jac
JOIN JOIN
jilo_agent ja ON jac.agent_id = ja.id jilo_agents ja ON jac.agent_id = ja.id
JOIN JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN JOIN
host h ON ja.host_id = h.id hosts h ON ja.host_id = h.id
WHERE WHERE
h.id = :host_id h.id = :host_id
AND jat.description = :agent_type AND jat.description = :agent_type

View File

@ -94,11 +94,11 @@ class Component {
$result = $stmt->fetchAll(PDO::FETCH_ASSOC); $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($result)) { if (!empty($result)) {
$logObject->log('info', "Retrieved " . count($result) . " Jitsi component events", ['user_id' => $userId, 'scope' => 'system']); $logObject->insertLog(0, "Retrieved " . count($result) . " Jitsi component events");
} }
return $result; return $result;
} catch (PDOException $e) { } catch (PDOException $e) {
$logObject->log('error', "Failed to retrieve Jitsi component events: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); $logObject->insertLog(0, "Failed to retrieve Jitsi component events: " . $e->getMessage());
return []; return [];
} }
} }
@ -162,7 +162,7 @@ class Component {
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
return (int)$result['total']; return (int)$result['total'];
} catch (PDOException $e) { } catch (PDOException $e) {
$logObject->log('error', "Failed to retrieve component events count: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); $logObject->insertLog(0, "Failed to retrieve component events count: " . $e->getMessage());
return 0; return 0;
} }
} }

View File

@ -16,7 +16,7 @@ class Config {
* @return array Returns an array with 'success' and 'updated' keys on success, or 'success' and 'error' keys on failure. * @return array Returns an array with 'success' and 'updated' keys on success, or 'success' and 'error' keys on failure.
*/ */
public function editConfigFile($updatedConfig, $config_file) { public function editConfigFile($updatedConfig, $config_file) {
global $logObject, $userId; global $logObject, $user_id;
$allLogs = []; $allLogs = [];
$updated = []; $updated = [];
@ -140,7 +140,7 @@ class Config {
} }
if (!empty($allLogs)) { if (!empty($allLogs)) {
$logObject->log('info', implode("\n", $allLogs), ['user_id' => $userId, 'scope' => 'system']); $logObject->insertLog($user_id, implode("\n", $allLogs), 'system');
} }
return [ return [
@ -148,7 +148,7 @@ class Config {
'updated' => $updated 'updated' => $updated
]; ];
} catch (Exception $e) { } catch (Exception $e) {
$logObject->log('error', "Config update error: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); $logObject->insertLog($user_id, "Config update error: " . $e->getMessage(), 'system');
return [ return [
'success' => false, 'success' => false,
'error' => $e->getMessage() 'error' => $e->getMessage()

View File

@ -35,10 +35,6 @@ class Feedback {
'type' => self::TYPE_SUCCESS, 'type' => self::TYPE_SUCCESS,
'dismissible' => true 'dismissible' => true
], ],
'SESSION_TIMEOUT' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'IP_BLACKLISTED' => [ 'IP_BLACKLISTED' => [
'type' => self::TYPE_ERROR, 'type' => self::TYPE_ERROR,
'dismissible' => false 'dismissible' => false
@ -53,21 +49,6 @@ class Feedback {
] ]
]; ];
const REGISTER = [
'SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'FAILED' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'DISABLED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
];
const SECURITY = [ const SECURITY = [
'WHITELIST_ADD_SUCCESS' => [ 'WHITELIST_ADD_SUCCESS' => [
'type' => self::TYPE_SUCCESS, 'type' => self::TYPE_SUCCESS,
@ -115,15 +96,19 @@ class Feedback {
] ]
]; ];
const THEME = [ const REGISTER = [
'THEME_CHANGE_SUCCESS' => [ 'SUCCESS' => [
'type' => self::TYPE_SUCCESS, 'type' => self::TYPE_SUCCESS,
'dismissible' => true 'dismissible' => true
], ],
'THEME_CHANGE_FAILED' => [ 'FAILED' => [
'type' => self::TYPE_ERROR, 'type' => self::TYPE_ERROR,
'dismissible' => true 'dismissible' => true
] ],
'DISABLED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
]; ];
const SYSTEM = [ const SYSTEM = [

View File

@ -37,7 +37,7 @@ class Host {
platform_id, platform_id,
name name
FROM FROM
host'; hosts';
if ($platform_id !== '' && $host_id !== '') { if ($platform_id !== '' && $host_id !== '') {
$sql .= ' WHERE platform_id = :platform_id AND id = :host_id'; $sql .= ' WHERE platform_id = :platform_id AND id = :host_id';
@ -71,7 +71,7 @@ class Host {
*/ */
public function addHost($newHost) { public function addHost($newHost) {
try { try {
$sql = 'INSERT INTO host $sql = 'INSERT INTO hosts
(address, platform_id, name) (address, platform_id, name)
VALUES VALUES
(:address, :platform_id, :name)'; (:address, :platform_id, :name)';
@ -101,7 +101,7 @@ class Host {
*/ */
public function editHost($platform_id, $updatedHost) { public function editHost($platform_id, $updatedHost) {
try { try {
$sql = 'UPDATE host SET $sql = 'UPDATE hosts SET
address = :address, address = :address,
name = :name name = :name
WHERE WHERE
@ -140,13 +140,13 @@ class Host {
$this->db->beginTransaction(); $this->db->beginTransaction();
// First delete all agents associated with this host // First delete all agents associated with this host
$sql = 'DELETE FROM jilo_agent WHERE host_id = :host_id'; $sql = 'DELETE FROM jilo_agents WHERE host_id = :host_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->bindParam(':host_id', $host_id); $query->bindParam(':host_id', $host_id);
$query->execute(); $query->execute();
// Then delete the host // Then delete the host
$sql = 'DELETE FROM host WHERE id = :host_id'; $sql = 'DELETE FROM hosts WHERE id = :host_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->bindParam(':host_id', $host_id); $query->bindParam(':host_id', $host_id);
$query->execute(); $query->execute();

View File

@ -1,40 +1,122 @@
<?php <?php
/** /**
* Log wrapper that delegates to plugin Log or NullLogger fallback. * class Log
* Used when code does require_once '../app/classes/log.php'. *
* Handles logging events into a database and reading log entries.
*/ */
// If there is already a Log plugin loaded
if (class_exists('Log')) {
return;
}
// Load fallback NullLogger
require_once __DIR__ . '/../core/NullLogger.php';
class Log { class Log {
private $logger; /**
* @var PDO|null $db The database connection instance.
*/
private $db;
/** /**
* @param mixed $database Database or DatabaseConnector instance * Logs constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/ */
public function __construct($database) { public function __construct($database) {
global $logObject; $this->db = $database->getConnection();
if (isset($logObject) && method_exists($logObject, 'insertLog')) { }
$this->logger = $logObject;
} else { /**
$this->logger = new \App\Core\NullLogger(); * Insert a log event into the database.
*
* @param int $user_id 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($user_id, $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' => $user_id,
':scope' => $scope,
':message' => $message,
]);
return true;
} catch (Exception $e) {
return $e->getMessage();
} }
} }
/** /**
* PSR-3 compatible log method * Retrieve log entries from the database.
* @param string $level *
* @param string $message * @param int $user_id The ID of the user whose logs are being retrieved.
* @param array $context * @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 log(string $level, string $message, array $context = []): void { public function readLog($user_id, $scope, $offset=0, $items_per_page='', $filters=[]) {
$this->logger->log($level, $message, $context); $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'] = $user_id;
}
// 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);
} }
} }

View File

@ -26,8 +26,8 @@ class PasswordReset {
// Check if email exists // Check if email exists
$query = $this->db->prepare(" $query = $this->db->prepare("
SELECT u.id, um.email SELECT u.id, um.email
FROM user u FROM users u
JOIN user_meta um ON u.id = um.user_id JOIN users_meta um ON u.id = um.user_id
WHERE um.email = :email" WHERE um.email = :email"
); );
$query->bindParam(':email', $email); $query->bindParam(':email', $email);

View File

@ -30,7 +30,7 @@ class Platform {
* @return array An associative array containing platform details. * @return array An associative array containing platform details.
*/ */
public function getPlatformDetails($platform_id = '') { public function getPlatformDetails($platform_id = '') {
$sql = 'SELECT * FROM platform'; $sql = 'SELECT * FROM platforms';
if ($platform_id !== '') { if ($platform_id !== '') {
$sql .= ' WHERE id = :platform_id'; $sql .= ' WHERE id = :platform_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
@ -57,7 +57,7 @@ class Platform {
*/ */
public function addPlatform($newPlatform) { public function addPlatform($newPlatform) {
try { try {
$sql = 'INSERT INTO platform $sql = 'INSERT INTO platforms
(name, jitsi_url, jilo_database) (name, jitsi_url, jilo_database)
VALUES VALUES
(:name, :jitsi_url, :jilo_database)'; (:name, :jitsi_url, :jilo_database)';
@ -90,7 +90,7 @@ class Platform {
*/ */
public function editPlatform($platform_id, $updatedPlatform) { public function editPlatform($platform_id, $updatedPlatform) {
try { try {
$sql = 'UPDATE platform SET $sql = 'UPDATE platforms SET
name = :name, name = :name,
jitsi_url = :jitsi_url, jitsi_url = :jitsi_url,
jilo_database = :jilo_database jilo_database = :jilo_database
@ -125,7 +125,7 @@ class Platform {
$this->db->beginTransaction(); $this->db->beginTransaction();
// First, get all hosts in this platform // First, get all hosts in this platform
$sql = 'SELECT id FROM host WHERE platform_id = :platform_id'; $sql = 'SELECT id FROM hosts WHERE platform_id = :platform_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id); $query->bindParam(':platform_id', $platform_id);
$query->execute(); $query->execute();
@ -133,20 +133,20 @@ class Platform {
// Delete all agents for each host // Delete all agents for each host
foreach ($hosts as $host) { foreach ($hosts as $host) {
$sql = 'DELETE FROM jilo_agent WHERE host_id = :host_id'; $sql = 'DELETE FROM jilo_agents WHERE host_id = :host_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->bindParam(':host_id', $host['id']); $query->bindParam(':host_id', $host['id']);
$query->execute(); $query->execute();
} }
// Delete all hosts in this platform // Delete all hosts in this platform
$sql = 'DELETE FROM host WHERE platform_id = :platform_id'; $sql = 'DELETE FROM hosts WHERE platform_id = :platform_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id); $query->bindParam(':platform_id', $platform_id);
$query->execute(); $query->execute();
// Finally, delete the platform // Finally, delete the platform
$sql = 'DELETE FROM platform WHERE id = :platform_id'; $sql = 'DELETE FROM platforms WHERE id = :platform_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id); $query->bindParam(':platform_id', $platform_id);
$query->execute(); $query->execute();

View File

@ -1,20 +1,16 @@
<?php <?php
use App\Core\NullLogger;
class RateLimiter { class RateLimiter {
public $db; public $db;
private $database; private $log;
/** @var mixed NullLogger (or PSR-3 logger) or plugin Log */
private $logger;
public $maxAttempts = 5; // Maximum login attempts public $maxAttempts = 5; // Maximum login attempts
public $decayMinutes = 15; // Time window in minutes public $decayMinutes = 15; // Time window in minutes
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
public $autoBlacklistDuration = 24; // Hours to blacklist for public $autoBlacklistDuration = 24; // Hours to blacklist for
public $authRatelimitTable = 'security_rate_auth'; // For rate limiting username/password attempts public $authRatelimitTable = 'login_attempts'; // For username/password attempts
public $pagesRatelimitTable = 'security_rate_page'; // For rate limiting page requests public $pagesRatelimitTable = 'pages_rate_limits'; // For page requests
public $whitelistTable = 'security_ip_whitelist'; // For whitelisting IPs and network ranges public $whitelistTable = 'ip_whitelist';
public $blacklistTable = 'security_ip_blacklist'; // For blacklisting IPs and network ranges public $blacklistTable = 'ip_blacklist';
private $pageLimits = [ private $pageLimits = [
// Default rate limits per minute // Default rate limits per minute
'default' => 60, 'default' => 60,
@ -26,23 +22,9 @@ class RateLimiter {
'config' => 10 'config' => 10
]; ];
/** public function __construct($database) {
* @param mixed $database Database object
* @param mixed $logger Optional NullLogger (or PSR-3 logger) or plugin Log
*/
public function __construct($database, $logger = null) {
$this->database = $database;
$this->db = $database->getConnection(); $this->db = $database->getConnection();
// Initialize logger (plugin Log if present or NullLogger otherwise) $this->log = new Log($database);
if ($logger !== null) {
$this->logger = $logger;
} else {
global $logObject;
$this->logger = isset($logObject) && is_object($logObject) && method_exists($logObject, 'info')
? $logObject
: new NullLogger();
}
// Initialize database tables
$this->createTablesIfNotExist(); $this->createTablesIfNotExist();
} }
@ -50,47 +32,42 @@ class RateLimiter {
private function createTablesIfNotExist() { private function createTablesIfNotExist() {
// Authentication attempts table // Authentication attempts table
$sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} ( $sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
id int(11) PRIMARY KEY AUTO_INCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address VARCHAR(45) NOT NULL, ip_address TEXT NOT NULL,
username VARCHAR(255) NOT NULL, username TEXT NOT NULL,
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP, attempted_at TEXT DEFAULT (DATETIME('now'))
INDEX idx_ip_username (ip_address, username)
)"; )";
$this->db->exec($sql); $this->db->exec($sql);
// Pages rate limits table // Pages rate limits table
$sql = "CREATE TABLE IF NOT EXISTS {$this->pagesRatelimitTable} ( $sql = "CREATE TABLE IF NOT EXISTS {$this->pagesRatelimitTable} (
id int(11) PRIMARY KEY AUTO_INCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address VARCHAR(45) NOT NULL, ip_address TEXT NOT NULL,
endpoint VARCHAR(255) NOT NULL, endpoint TEXT NOT NULL,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP, request_time DATETIME DEFAULT CURRENT_TIMESTAMP
INDEX idx_ip_endpoint (ip_address, endpoint),
INDEX idx_request_time (request_time)
)"; )";
$this->db->exec($sql); $this->db->exec($sql);
// IP whitelist table // IP whitelist table
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} ( $sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
id int(11) PRIMARY KEY AUTO_INCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address VARCHAR(45) NOT NULL, ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT FALSE, is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description VARCHAR(255), description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (DATETIME('now')),
created_by VARCHAR(255), created_by TEXT
UNIQUE KEY unique_ip (ip_address)
)"; )";
$this->db->exec($sql); $this->db->exec($sql);
// IP blacklist table // IP blacklist table
$sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} ( $sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} (
id int(11) PRIMARY KEY AUTO_INCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address VARCHAR(45) NOT NULL, ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT FALSE, is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason VARCHAR(255), reason TEXT,
expiry_time TIMESTAMP NULL, expiry_time TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (DATETIME('now')),
created_by VARCHAR(255), created_by TEXT
UNIQUE KEY unique_ip (ip_address)
)"; )";
$this->db->exec($sql); $this->db->exec($sql);
@ -104,7 +81,7 @@ class RateLimiter {
]; ];
// Insert default whitelisted IPs if they don't exist // Insert default whitelisted IPs if they don't exist
$stmt = $this->db->prepare("INSERT IGNORE INTO {$this->whitelistTable} $stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->whitelistTable}
(ip_address, is_network, description, created_by) (ip_address, is_network, description, created_by)
VALUES (?, ?, ?, 'system')"); VALUES (?, ?, ?, 'system')");
foreach ($defaultIps as $ip) { foreach ($defaultIps as $ip) {
@ -120,7 +97,7 @@ class RateLimiter {
['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737'] ['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737']
]; ];
$stmt = $this->db->prepare("INSERT IGNORE INTO {$this->blacklistTable} $stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->blacklistTable}
(ip_address, is_network, reason, created_by) (ip_address, is_network, reason, created_by)
VALUES (?, ?, ?, 'system')"); VALUES (?, ?, ?, 'system')");
@ -135,7 +112,7 @@ class RateLimiter {
*/ */
public function getRecentAttempts($ip) { public function getRecentAttempts($ip) {
$stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->authRatelimitTable} $stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->authRatelimitTable}
WHERE ip_address = ? AND attempted_at > DATE_SUB(NOW(), INTERVAL ? MINUTE)"); WHERE ip_address = ? AND attempted_at > datetime('now', '-' || :minutes || ' minutes')");
$stmt->execute([$ip, $this->decayMinutes]); $stmt->execute([$ip, $this->decayMinutes]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
return intval($result['attempts']); return intval($result['attempts']);
@ -232,19 +209,15 @@ class RateLimiter {
if ($this->isIpBlacklisted($ip)) { if ($this->isIpBlacklisted($ip)) {
$message = "Cannot whitelist {$ip} - IP is currently blacklisted"; $message = "Cannot whitelist {$ip} - IP is currently blacklisted";
if ($userId) { if ($userId) {
$this->logger->log('info', "IP Whitelist: {$message}", ['user_id' => $userId, 'scope' => 'system']); $this->log->insertLog($userId, "IP Whitelist: {$message}", 'system');
Feedback::flash('ERROR', 'DEFAULT', $message); Feedback::flash('ERROR', 'DEFAULT', $message);
} }
return false; return false;
} }
$stmt = $this->db->prepare("INSERT INTO {$this->whitelistTable} $stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->whitelistTable}
(ip_address, is_network, description, created_by) (ip_address, is_network, description, created_by)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)");
ON DUPLICATE KEY UPDATE
is_network = VALUES(is_network),
description = VALUES(description),
created_by = VALUES(created_by)");
$result = $stmt->execute([$ip, $isNetwork, $description, $createdBy]); $result = $stmt->execute([$ip, $isNetwork, $description, $createdBy]);
@ -256,14 +229,14 @@ class RateLimiter {
$createdBy, $createdBy,
$description $description
); );
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']); $this->log->insertLog($userId ?? 0, $logMessage, 'system');
} }
return $result; return $result;
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->logger->log('error', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); $this->log->insertLog($userId, "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), 'system');
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage());
} }
return false; return false;
@ -291,14 +264,14 @@ class RateLimiter {
$removedBy, $removedBy,
$ipDetails['created_by'] $ipDetails['created_by']
); );
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']); $this->log->insertLog($userId ?? 0, $logMessage, 'system');
} }
return $result; return $result;
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->logger->log('error', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); $this->log->insertLog($userId, "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), 'system');
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage());
} }
return false; return false;
@ -311,7 +284,7 @@ class RateLimiter {
if ($this->isIpWhitelisted($ip)) { if ($this->isIpWhitelisted($ip)) {
$message = "Cannot blacklist {$ip} - IP is currently whitelisted"; $message = "Cannot blacklist {$ip} - IP is currently whitelisted";
if ($userId) { if ($userId) {
$this->logger->log('info', "IP Blacklist: {$message}", ['user_id' => $userId, 'scope' => 'system']); $this->log->insertLog($userId, "IP Blacklist: {$message}", 'system');
Feedback::flash('ERROR', 'DEFAULT', $message); Feedback::flash('ERROR', 'DEFAULT', $message);
} }
return false; return false;
@ -319,14 +292,9 @@ class RateLimiter {
$expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null; $expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null;
$stmt = $this->db->prepare("INSERT INTO {$this->blacklistTable} $stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->blacklistTable}
(ip_address, is_network, reason, expiry_time, created_by) (ip_address, is_network, reason, expiry_time, created_by)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)");
ON DUPLICATE KEY UPDATE
is_network = VALUES(is_network),
reason = VALUES(reason),
expiry_time = VALUES(expiry_time),
created_by = VALUES(created_by)");
$result = $stmt->execute([$ip, $isNetwork, $reason, $expiryTime, $createdBy]); $result = $stmt->execute([$ip, $isNetwork, $reason, $expiryTime, $createdBy]);
@ -339,13 +307,13 @@ class RateLimiter {
$reason, $reason,
$expiryTime ?? 'never' $expiryTime ?? 'never'
); );
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']); $this->log->insertLog($userId ?? 0, $logMessage, 'system');
} }
return $result; return $result;
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->logger->log('error', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); $this->log->insertLog($userId, "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), 'system');
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage());
} }
return false; return false;
@ -373,13 +341,13 @@ class RateLimiter {
$ipDetails['created_by'], $ipDetails['created_by'],
$ipDetails['reason'] $ipDetails['reason']
); );
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']); $this->log->insertLog($userId ?? 0, $logMessage, 'system');
} }
return $result; return $result;
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->logger->log('error', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); $this->log->insertLog($userId, "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), 'system');
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage());
} }
return false; return false;
@ -404,17 +372,17 @@ class RateLimiter {
try { try {
// Remove expired blacklist entries // Remove expired blacklist entries
$stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable} $stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable}
WHERE expiry_time IS NOT NULL AND expiry_time < NOW()"); WHERE expiry_time IS NOT NULL AND expiry_time < datetime('now')");
$stmt->execute(); $stmt->execute();
// Clean old login attempts // Clean old login attempts
$stmt = $this->db->prepare("DELETE FROM {$this->authRatelimitTable} $stmt = $this->db->prepare("DELETE FROM {$this->authRatelimitTable}
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)"); WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')");
$stmt->execute([':minutes' => $this->decayMinutes]); $stmt->execute([':minutes' => $this->decayMinutes]);
return true; return true;
} catch (Exception $e) { } catch (Exception $e) {
$this->logger->log('error', "Failed to cleanup expired entries: " . $e->getMessage(), ['user_id' => $userId ?? null, 'scope' => 'system']); $this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system');
Feedback::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage());
return false; return false;
} }
@ -443,7 +411,7 @@ class RateLimiter {
$sql = "SELECT COUNT(*) as total_attempts $sql = "SELECT COUNT(*) as total_attempts
FROM {$this->authRatelimitTable} FROM {$this->authRatelimitTable}
WHERE ip_address = :ip WHERE ip_address = :ip
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)"; AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
':ip' => $ipAddress, ':ip' => $ipAddress,
@ -455,12 +423,7 @@ class RateLimiter {
return $result['total_attempts'] < $this->autoBlacklistThreshold; return $result['total_attempts'] < $this->autoBlacklistThreshold;
} }
public function attempt($username, $ipAddress, $failed = true) { public function attempt($username, $ipAddress) {
// Only record failed attempts
if (!$failed) {
return true;
}
// Record this attempt // Record this attempt
$sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)"; $sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
@ -481,7 +444,7 @@ class RateLimiter {
FROM {$this->authRatelimitTable} FROM {$this->authRatelimitTable}
WHERE ip_address = :ip WHERE ip_address = :ip
AND username = :username AND username = :username
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)"; AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -517,7 +480,7 @@ class RateLimiter {
public function clearOldAttempts() { public function clearOldAttempts() {
$sql = "DELETE FROM {$this->authRatelimitTable} $sql = "DELETE FROM {$this->authRatelimitTable}
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)"; WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -530,7 +493,7 @@ class RateLimiter {
FROM {$this->authRatelimitTable} FROM {$this->authRatelimitTable}
WHERE ip_address = :ip WHERE ip_address = :ip
AND username = :username AND username = :username
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)"; AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -572,7 +535,7 @@ class RateLimiter {
FROM {$this->pagesRatelimitTable} FROM {$this->pagesRatelimitTable}
WHERE ip_address = :ip WHERE ip_address = :ip
AND endpoint = :endpoint AND endpoint = :endpoint
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)"; AND request_time >= DATETIME('now', '-1 minute')";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -603,7 +566,7 @@ class RateLimiter {
*/ */
private function cleanOldPageRequests() { private function cleanOldPageRequests() {
$sql = "DELETE FROM {$this->pagesRatelimitTable} $sql = "DELETE FROM {$this->pagesRatelimitTable}
WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)"; WHERE request_time < DATETIME('now', '-1 minute')";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute(); $stmt->execute();
@ -615,10 +578,8 @@ class RateLimiter {
private function getPageLimitForEndpoint($endpoint, $userId = null) { private function getPageLimitForEndpoint($endpoint, $userId = null) {
// Admin users get higher limits // Admin users get higher limits
if ($userId) { if ($userId) {
// Check admin rights directly from database $userObj = new User($this->db);
$stmt = $this->db->prepare('SELECT COUNT(*) FROM `user_right` ur JOIN `right` r ON ur.right_id = r.id WHERE ur.user_id = ? AND r.name = ?'); if ($userObj->hasRight($userId, 'admin')) {
$stmt->execute([$userId, 'superuser']);
if ($stmt->fetchColumn() > 0) {
return $this->pageLimits['admin']; return $this->pageLimits['admin'];
} }
} }
@ -654,7 +615,7 @@ class RateLimiter {
FROM {$this->pagesRatelimitTable} FROM {$this->pagesRatelimitTable}
WHERE ip_address = :ip WHERE ip_address = :ip
AND endpoint = :endpoint AND endpoint = :endpoint
AND request_time > DATE_SUB(NOW(), INTERVAL 1 MINUTE)"; AND request_time > DATETIME('now', '-1 minute')";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([

View File

@ -1,265 +0,0 @@
<?php
/**
* Session Class
*
* Core session management functionality for the application
*/
class Session {
private static $initialized = false;
private static $sessionName = ''; // Will be set from config, if not we'll have a random session name
/**
* Generate a random session name
*/
private static function generateRandomSessionName(): string {
return 'sess_' . bin2hex(random_bytes(8)); // 16-character random string
}
private static $sessionOptions = [
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200 // 2 hours
];
/**
* Initialize session configuration
*/
private static function initialize() {
if (self::$initialized) {
return;
}
global $config;
// Get session name from config or generate a random one
self::$sessionName = $config['session']['name'] ?? self::generateRandomSessionName();
// Set session name before starting the session
session_name(self::$sessionName);
// Set session cookie parameters
$thisPath = $config['folder'] ?? '/';
$thisDomain = $config['domain'] ?? '';
$isSecure = isset($_SERVER['HTTPS']);
session_set_cookie_params([
'lifetime' => 0, // Session cookie (browser session)
'path' => $thisPath,
'domain' => $thisDomain,
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Strict'
]);
self::$initialized = true;
}
/**
* Get session name from config or generate a random one
*/
private static function getSessionNameFromConfig($config) {
if (isset($config['session']['name']) && !empty($config['session']['name'])) {
return $config['session']['name'];
}
return self::generateRandomSessionName();
}
/**
* Start or resume a session with secure options
*/
public static function startSession() {
self::initialize();
if (session_status() === PHP_SESSION_NONE) {
if (!headers_sent()) {
session_start(self::$sessionOptions);
}
}
}
/**
* Destroy current session and clean up
*/
public static function destroySession() {
if (session_status() === PHP_SESSION_ACTIVE) {
session_unset();
session_destroy();
}
}
/**
* Get current username if set
*/
public static function getUsername() {
return isset($_SESSION['username']) ? htmlspecialchars($_SESSION['username']) : null;
}
/**
* Get current user ID if set
*/
public static function getUserId() {
return isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
}
/**
* Check if current session is valid
*
* @param bool $strict If true, will return false for new/unauthenticated sessions
* @return bool True if session is valid, false otherwise
*/
public static function isValidSession($strict = true) {
// If session is not started or empty, it's not valid
if (session_status() !== PHP_SESSION_ACTIVE || empty($_SESSION)) {
return false;
}
// In non-strict mode, consider empty session as valid (for login/logout)
if (!$strict && !isset($_SESSION['user_id']) && !isset($_SESSION['username'])) {
return true;
}
// In strict mode, require user_id and username
if ($strict && (!isset($_SESSION['user_id']) || !isset($_SESSION['username']))) {
return false;
}
// Check session timeout
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 7200; // 30 days or 2 hours
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) {
return false;
}
// Update last activity time
$_SESSION['LAST_ACTIVITY'] = time();
// Regenerate session ID periodically (every 30 minutes)
if (!isset($_SESSION['CREATED'])) {
$_SESSION['CREATED'] = time();
} else if (time() - $_SESSION['CREATED'] > 1800) {
// Regenerate session ID and update creation time
if (!headers_sent() && session_status() === PHP_SESSION_ACTIVE) {
$oldData = $_SESSION;
session_regenerate_id(true);
$_SESSION = $oldData;
$_SESSION['CREATED'] = time();
}
}
return true;
}
/**
* Set remember me option for extended session
*/
public static function setRememberMe($value = true) {
$_SESSION['REMEMBER_ME'] = $value;
}
/**
* Clear session data and cookies
*/
public static function cleanup($config) {
self::destroySession();
// Clear cookies if headers not sent
if (!headers_sent()) {
setcookie('username', '', [
'expires' => time() - 3600,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
}
// Start fresh session
self::startSession();
// Reset session timeout flag
unset($_SESSION['session_timeout_shown']);
}
/**
* Create a new authenticated session for a user
*/
public static function createAuthSession($userId, $username, $rememberMe, $config) {
// Ensure session is started
self::startSession();
// Set session variables
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
$_SESSION['REMEMBER_ME'] = $rememberMe;
// Set cookie lifetime based on remember me
$cookieLifetime = $rememberMe ? time() + (30 * 24 * 60 * 60) : 0;
// Update session cookie with remember me setting
if (!headers_sent()) {
setcookie(
session_name(),
session_id(),
[
'expires' => $cookieLifetime,
'path' => $config['folder'] ?? '/',
'domain' => $config['domain'] ?? '',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]
);
// Set username cookie
setcookie('username', $username, [
'expires' => $cookieLifetime,
'path' => $config['folder'] ?? '/',
'domain' => $config['domain'] ?? '',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
}
if ($rememberMe) {
self::setRememberMe(true);
}
}
/**
* Store 2FA pending information in session
*/
public static function store2FAPending($userId, $username, $rememberMe = false) {
$_SESSION['2fa_pending_user_id'] = $userId;
$_SESSION['2fa_pending_username'] = $username;
if ($rememberMe) {
$_SESSION['2fa_pending_remember'] = true;
}
}
/**
* Clear 2FA pending information from session
*/
public static function clear2FAPending() {
unset($_SESSION['2fa_pending_user_id']);
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
}
/**
* Get 2FA pending information
*/
public static function get2FAPending() {
if (!isset($_SESSION['2fa_pending_user_id']) || !isset($_SESSION['2fa_pending_username'])) {
return null;
}
return [
'user_id' => $_SESSION['2fa_pending_user_id'],
'username' => $_SESSION['2fa_pending_username'],
'remember_me' => isset($_SESSION['2fa_pending_remember'])
];
}
}

View File

@ -3,7 +3,7 @@
/** /**
* class User * class User
* *
* Handles user-related functionalities such as login, rights management, and profile updates. * Handles user-related functionalities such as registration, login, rights management, and profile updates.
*/ */
class User { class User {
/** /**
@ -33,6 +33,63 @@ class User {
} }
/**
* Registers a new user.
*
* @param string $username The username of the new user.
* @param string $password The password for the new user.
*
* @return bool|string True if registration is successful, error message otherwise.
*/
public function register($username, $password) {
try {
// we have two inserts, start a transaction
$this->db->beginTransaction();
// hash the password, don't store it plain
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// insert into users table
$sql = 'INSERT
INTO users (username, password)
VALUES (:username, :password)';
$query = $this->db->prepare($sql);
$query->bindValue(':username', $username);
$query->bindValue(':password', $hashedPassword);
// execute the first query
if (!$query->execute()) {
// rollback on error
$this->db->rollBack();
return false;
}
// insert the last user id into users_meta table
$sql2 = 'INSERT
INTO users_meta (user_id)
VALUES (:user_id)';
$query2 = $this->db->prepare($sql2);
$query2->bindValue(':user_id', $this->db->lastInsertId());
// execute the second query
if (!$query2->execute()) {
// rollback on error
$this->db->rollBack();
return false;
}
// if all is OK, commit the transaction
$this->db->commit();
return true;
} catch (Exception $e) {
// rollback on any error
$this->db->rollBack();
return $e->getMessage();
}
}
/** /**
* Logs in a user by verifying credentials. * Logs in a user by verifying credentials.
* *
@ -44,8 +101,12 @@ class User {
*/ */
public function login($username, $password, $twoFactorCode = null) { public function login($username, $password, $twoFactorCode = null) {
// Get user's IP address // Get user's IP address
require_once __DIR__ . '/../helpers/logs.php';
$ipAddress = getUserIP(); $ipAddress = getUserIP();
// Record attempt
$this->rateLimiter->attempt($username, $ipAddress);
// Check rate limiting first // Check rate limiting first
if (!$this->rateLimiter->isAllowed($username, $ipAddress)) { if (!$this->rateLimiter->isAllowed($username, $ipAddress)) {
$remainingTime = $this->rateLimiter->getDecayMinutes(); $remainingTime = $this->rateLimiter->getDecayMinutes();
@ -53,7 +114,7 @@ class User {
} }
// Then check credentials // Then check credentials
$query = $this->db->prepare("SELECT * FROM user WHERE username = :username"); $query = $this->db->prepare("SELECT * FROM users WHERE username = :username");
$query->bindParam(':username', $username); $query->bindParam(':username', $username);
$query->execute(); $query->execute();
@ -92,10 +153,7 @@ class User {
// Get remaining attempts AFTER this failed attempt // Get remaining attempts AFTER this failed attempt
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress); $remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
return [ throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining.");
'status' => 'failed',
'message' => "Invalid credentials. {$remainingAttempts} attempts remaining."
];
} }
@ -108,7 +166,7 @@ class User {
*/ */
// FIXME not used now? // FIXME not used now?
public function getUserId($username) { public function getUserId($username) {
$sql = 'SELECT id FROM user WHERE username = :username'; $sql = 'SELECT id FROM users WHERE username = :username';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->bindParam(':username', $username); $query->bindParam(':username', $username);
@ -122,24 +180,24 @@ class User {
/** /**
* Fetches user details by user ID. * Fetches user details by user ID.
* *
* @param int $userId The user ID. * @param int $user_id The user ID.
* *
* @return array|null User details or null if not found. * @return array|null User details or null if not found.
*/ */
public function getUserDetails($userId) { public function getUserDetails($user_id) {
$sql = 'SELECT $sql = 'SELECT
um.*, um.*,
u.username u.username
FROM FROM
user_meta um users_meta um
LEFT JOIN user u LEFT JOIN users u
ON um.user_id = u.id ON um.user_id = u.id
WHERE WHERE
u.id = :user_id'; u.id = :user_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $userId, ':user_id' => $user_id,
]); ]);
return $query->fetchAll(PDO::FETCH_ASSOC); return $query->fetchAll(PDO::FETCH_ASSOC);
@ -150,19 +208,19 @@ class User {
/** /**
* Grants a user a specific right. * Grants a user a specific right.
* *
* @param int $userId The user ID. * @param int $user_id The user ID.
* @param int $right_id The right ID to grant. * @param int $right_id The right ID to grant.
* *
* @return void * @return void
*/ */
public function addUserRight($userId, $right_id) { public function addUserRight($user_id, $right_id) {
$sql = 'INSERT INTO user_right $sql = 'INSERT INTO users_rights
(user_id, right_id) (user_id, right_id)
VALUES VALUES
(:user_id, :right_id)'; (:user_id, :right_id)';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $userId, ':user_id' => $user_id,
':right_id' => $right_id, ':right_id' => $right_id,
]); ]);
} }
@ -171,20 +229,20 @@ class User {
/** /**
* Revokes a specific right from a user. * Revokes a specific right from a user.
* *
* @param int $userId The user ID. * @param int $user_id The user ID.
* @param int $right_id The right ID to revoke. * @param int $right_id The right ID to revoke.
* *
* @return void * @return void
*/ */
public function removeUserRight($userId, $right_id) { public function removeUserRight($user_id, $right_id) {
$sql = 'DELETE FROM user_right $sql = 'DELETE FROM users_rights
WHERE WHERE
user_id = :user_id user_id = :user_id
AND AND
right_id = :right_id'; right_id = :right_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $userId, ':user_id' => $user_id,
':right_id' => $right_id, ':right_id' => $right_id,
]); ]);
} }
@ -199,7 +257,7 @@ class User {
$sql = 'SELECT $sql = 'SELECT
id AS right_id, id AS right_id,
name AS right_name name AS right_name
FROM `right` FROM rights
ORDER BY id ASC'; ORDER BY id ASC';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute(); $query->execute();
@ -212,27 +270,27 @@ class User {
/** /**
* Retrieves the rights assigned to a specific user. * Retrieves the rights assigned to a specific user.
* *
* @param int $userId The user ID. * @param int $user_id The user ID.
* *
* @return array List of user rights. * @return array List of user rights.
*/ */
public function getUserRights($userId) { public function getUserRights($user_id) {
$sql = 'SELECT $sql = 'SELECT
u.id AS user_id, u.id AS user_id,
r.id AS right_id, r.id AS right_id,
r.name AS right_name r.name AS right_name
FROM FROM
`user` u users u
LEFT JOIN `user_right` ur LEFT JOIN users_rights ur
ON u.id = ur.user_id ON u.id = ur.user_id
LEFT JOIN `right` r LEFT JOIN rights r
ON ur.right_id = r.id ON ur.right_id = r.id
WHERE WHERE
u.id = :user_id'; u.id = :user_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $userId, ':user_id' => $user_id,
]); ]);
$result = $query->fetchAll(PDO::FETCH_ASSOC); $result = $query->fetchAll(PDO::FETCH_ASSOC);
@ -241,7 +299,7 @@ class User {
$specialEntries = []; $specialEntries = [];
// user 1 is always superuser // user 1 is always superuser
if ($userId == 1) { if ($user_id == 1) {
$specialEntries = [ $specialEntries = [
[ [
'user_id' => 1, 'user_id' => 1,
@ -251,7 +309,7 @@ class User {
]; ];
// user 2 is always demo // user 2 is always demo
} elseif ($userId == 2) { } elseif ($user_id == 2) {
$specialEntries = [ $specialEntries = [
[ [
'user_id' => 2, 'user_id' => 2,
@ -275,17 +333,17 @@ class User {
/** /**
* Check if the user has a specific right. * Check if the user has a specific right.
* *
* @param int $userId The user ID. * @param int $user_id The user ID.
* @param string $right_name The human-readable name of the user right. * @param string $right_name The human-readable name of the user right.
* *
* @return bool True if the user has the right, false otherwise. * @return bool True if the user has the right, false otherwise.
*/ */
function hasRight($userId, $right_name) { function hasRight($user_id, $right_name) {
$userRights = $this->getUserRights($userId); $userRights = $this->getUserRights($user_id);
$userHasRight = false; $userHasRight = false;
// superuser always has all the rights // superuser always has all the rights
if ($userId === 1) { if ($user_id === 1) {
$userHasRight = true; $userHasRight = true;
} }
@ -304,8 +362,8 @@ class User {
/** /**
* Updates a user's metadata in the database. * Updates a user's metadata in the database.
* *
* @param int $userId The ID of the user to update. * @param int $user_id The ID of the user to update.
* @param array $updatedUser An associative array containing updated user data: * @param array $updatedUser An associative array containing updated user data:
* - 'name' (string): The updated name of the user. * - 'name' (string): The updated name of the user.
* - 'email' (string): The updated email of the user. * - 'email' (string): The updated email of the user.
* - 'timezone' (string): The updated timezone of the user. * - 'timezone' (string): The updated timezone of the user.
@ -313,9 +371,9 @@ class User {
* *
* @return bool|string Returns true if the update is successful, or an error message if an exception occurs. * @return bool|string Returns true if the update is successful, or an error message if an exception occurs.
*/ */
public function editUser($userId, $updatedUser) { public function editUser($user_id, $updatedUser) {
try { try {
$sql = 'UPDATE user_meta SET $sql = 'UPDATE users_meta SET
name = :name, name = :name,
email = :email, email = :email,
timezone = :timezone, timezone = :timezone,
@ -323,7 +381,7 @@ class User {
WHERE user_id = :user_id'; WHERE user_id = :user_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $userId, ':user_id' => $user_id,
':name' => $updatedUser['name'], ':name' => $updatedUser['name'],
':email' => $updatedUser['email'], ':email' => $updatedUser['email'],
':timezone' => $updatedUser['timezone'], ':timezone' => $updatedUser['timezone'],
@ -342,20 +400,20 @@ class User {
/** /**
* Removes a user's avatar from the database and deletes the associated file. * Removes a user's avatar from the database and deletes the associated file.
* *
* @param int $userId The ID of the user whose avatar is being removed. * @param int $user_id The ID of the user whose avatar is being removed.
* @param string $old_avatar Optional. The file path of the current avatar to delete. Default is an empty string. * @param string $old_avatar Optional. The file path of the current avatar to delete. Default is an empty string.
* *
* @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs. * @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs.
*/ */
public function removeAvatar($userId, $old_avatar = '') { public function removeAvatar($user_id, $old_avatar = '') {
try { try {
// remove from database // remove from database
$sql = 'UPDATE user_meta SET $sql = 'UPDATE users_meta SET
avatar = NULL avatar = NULL
WHERE user_id = :user_id'; WHERE user_id = :user_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':user_id' => $userId, ':user_id' => $user_id,
]); ]);
// delete the old avatar file // delete the old avatar file
@ -375,14 +433,14 @@ class User {
/** /**
* Updates a user's avatar by uploading a new file and saving its path in the database. * Updates a user's avatar by uploading a new file and saving its path in the database.
* *
* @param int $userId The ID of the user whose avatar is being updated. * @param int $user_id The ID of the user whose avatar is being updated.
* @param array $avatar_file The uploaded avatar file from the $_FILES array. * @param array $avatar_file The uploaded avatar file from the $_FILES array.
* Should include 'tmp_name', 'name', 'error', etc. * Should include 'tmp_name', 'name', 'error', etc.
* @param string $avatars_path The directory path where avatar files should be saved. * @param string $avatars_path The directory path where avatar files should be saved.
* *
* @return bool|string Returns true if the avatar is successfully updated, or an error message if an exception occurs. * @return bool|string Returns true if the avatar is successfully updated, or an error message if an exception occurs.
*/ */
public function changeAvatar($userId, $avatar_file, $avatars_path) { public function changeAvatar($user_id, $avatar_file, $avatars_path) {
try { try {
// check if the file was uploaded // check if the file was uploaded
if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) { if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) {
@ -399,13 +457,13 @@ class User {
if (move_uploaded_file($fileTmpPath, $dest_path)) { if (move_uploaded_file($fileTmpPath, $dest_path)) {
try { try {
// update user's avatar path in DB // update user's avatar path in DB
$sql = 'UPDATE user_meta SET $sql = 'UPDATE users_meta SET
avatar = :avatar avatar = :avatar
WHERE user_id = :user_id'; WHERE user_id = :user_id';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([ $query->execute([
':avatar' => $newFileName, ':avatar' => $newFileName,
':user_id' => $userId ':user_id' => $user_id
]); ]);
// all went OK // all went OK
$_SESSION['notice'] .= 'Avatar updated successfully. '; $_SESSION['notice'] .= 'Avatar updated successfully. ';
@ -435,7 +493,7 @@ class User {
*/ */
public function getUsers() { public function getUsers() {
$sql = "SELECT id, username $sql = "SELECT id, username
FROM `user` FROM users
ORDER BY username ASC"; ORDER BY username ASC";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
@ -447,9 +505,9 @@ class User {
/** /**
* Enable two-factor authentication for a user * Enable two-factor authentication for a user
* *
* @param int $userId User ID * @param int $userId User ID
* @param string $secret Secret key to use * @param string $secret Secret key to use
* @param string $code Verification code to validate * @param string $code Verification code to validate
* @return bool True if enabled successfully * @return bool True if enabled successfully
*/ */
public function enableTwoFactor($userId, $secret = null, $code = null) { public function enableTwoFactor($userId, $secret = null, $code = null) {
@ -469,8 +527,8 @@ class User {
/** /**
* Verify a two-factor authentication code * Verify a two-factor authentication code
* *
* @param int $userId User ID * @param int $userId User ID
* @param string $code The verification code * @param string $code The verification code
* @return bool True if verified * @return bool True if verified
*/ */
public function verifyTwoFactor($userId, $code) { public function verifyTwoFactor($userId, $code) {
@ -490,15 +548,15 @@ class User {
/** /**
* Change a user's password * Change a user's password
* *
* @param int $userId User ID * @param int $userId User ID
* @param string $currentPassword Current password for verification * @param string $currentPassword Current password for verification
* @param string $newPassword New password to set * @param string $newPassword New password to set
* @return bool True if password was changed successfully * @return bool True if password was changed successfully
*/ */
public function changePassword($userId, $currentPassword, $newPassword) { public function changePassword($userId, $currentPassword, $newPassword) {
try { try {
// First verify the current password // First verify the current password
$sql = "SELECT password FROM user WHERE id = :user_id"; $sql = "SELECT password FROM users WHERE id = :user_id";
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute([':user_id' => $userId]); $query->execute([':user_id' => $userId]);
$user = $query->fetch(PDO::FETCH_ASSOC); $user = $query->fetch(PDO::FETCH_ASSOC);
@ -511,7 +569,7 @@ class User {
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT); $hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
// Update the password // Update the password
$sql = "UPDATE user SET password = :password WHERE id = :user_id"; $sql = "UPDATE users SET password = :password WHERE id = :user_id";
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
return $query->execute([ return $query->execute([
':password' => $hashedPassword, ':password' => $hashedPassword,

View File

@ -10,17 +10,8 @@ return [
'domain' => 'localhost', 'domain' => 'localhost',
// subfolder for the web app, if any // subfolder for the web app, if any
'folder' => '/jilo-web/', 'folder' => '/jilo-web/',
// site name used in emails and in the interface // site name used in emails and in the inteerface
'site_name' => 'Jilo Web', 'site_name' => 'Jilo Web',
// session configuration
'session' => [
// session name, if empty a random one will be generated
'name' => 'jilo',
// 2 hours (7200) default, when "remember me" is not checked
'lifetime' => 7200,
// 30 days (2592000) default, when "remember me" is checked
'remember_me_lifetime' => 2592000,
],
// set to false to disable new registrations // set to false to disable new registrations
'registration_enabled' => true, 'registration_enabled' => true,
// will be displayed on login screen // will be displayed on login screen
@ -31,20 +22,12 @@ return [
//******************************************* //*******************************************
// database // database
'db_type' => 'mariadb', 'db' => [
// DB type for the web app, currently only "sqlite" is used
'sqlite' => [ 'db_type' => 'sqlite',
// default is ../app/jilo-web.db
'sqlite_file' => '../app/jilo-web.db', 'sqlite_file' => '../app/jilo-web.db',
], ],
'sql' => [
'sql_host' => 'localhost',
'sql_port' => '3306',
'sql_database' => 'jilo',
'sql_username' => 'jilouser',
'sql_password' => 'jilopass',
],
// avatars path // avatars path
'avatars_path' => 'uploads/avatars/', 'avatars_path' => 'uploads/avatars/',
// default avatar // default avatar

View File

@ -1,35 +0,0 @@
<?php
return [
// Active theme (can be overridden by user preference)
'active_theme' => 'default',
// Available themes with their display names
'available_themes' => [
'default' => 'Default built-in theme',
'modern' => 'Modern theme',
'retro' => 'Alternative retro theme'
],
// Path configurations
'paths' => [
// Base directory for all external themes
'themes' => __DIR__ . '/../../themes',
// Default templates location (built-in fallback)
'templates' => __DIR__ . '/../templates',
// Public assets directory (built-in fallback)
'public' => __DIR__ . '/../../public_html'
],
// Theme configuration defaults
'default_config' => [
'name' => 'Unnamed Theme',
'description' => 'A Jilo Web theme',
'version' => '1.0.0',
'author' => 'Lindeas Inc.',
'screenshot' => 'screenshot.png',
'options' => []
]
];

View File

@ -1,41 +0,0 @@
<?php
namespace App\Core;
class ConfigLoader
{
/**
* @var string|null
*/
private static $configPath = null;
/**
* Load configuration array from a set of possible file locations.
*
* @param string[] $locations
* @return array
*/
public static function loadConfig(array $locations): array
{
$configFile = null;
foreach ($locations as $location) {
if (file_exists($location)) {
$configFile = $location;
break;
}
}
if (!$configFile) {
die('Config file not found');
}
self::$configPath = $configFile;
return require $configFile;
}
/**
* @return string|null
*/
public static function getConfigPath(): ?string
{
return self::$configPath;
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace App\Core;
use Exception;
use Feedback;
class DatabaseConnector
{
/**
* Connect to the database using given configuration and handle errors.
*
* @param array $config
* @return mixed Database connection
*/
public static function connect(array $config)
{
// Load DB classes
require_once __DIR__ . '/../classes/database.php';
require_once __DIR__ . '/../includes/database.php';
try {
$db = connectDB($config);
if (!$db) {
throw new Exception('Could not connect to database');
}
return $db;
} catch (Exception $e) {
// Show error and exit
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to the database.', $e->getMessage()));
include __DIR__ . '/../templates/page-header.php';
include __DIR__ . '/../helpers/feedback.php';
include __DIR__ . '/../templates/page-footer.php';
exit();
}
}
}

View File

@ -1,53 +0,0 @@
<?php
namespace App\Core;
class HookDispatcher
{
/**
* Stores all registered hooks and their callbacks.
* @var array<string, array<callable>>
*/
private static array $hooks = [];
/**
* Register a callback for a given hook.
*/
public static function register(string $hook, callable $callback): void
{
if (!isset(self::$hooks[$hook])) {
self::$hooks[$hook] = [];
}
self::$hooks[$hook][] = $callback;
}
/**
* Dispatch all callbacks for the specified hook.
*/
public static function dispatch(string $hook, array $context = []): void
{
if (!empty(self::$hooks[$hook])) {
foreach (self::$hooks[$hook] as $callback) {
call_user_func($callback, $context);
}
}
}
/**
* Apply filters for a hook key, passing a value through all callbacks.
* Each callback should accept the value and return a modified value.
*
* @param string $hook
* @param mixed $value
* @return mixed
*/
public static function applyFilters(string $hook, $value)
{
if (!empty(self::$hooks[$hook])) {
foreach (self::$hooks[$hook] as $callback) {
$value = call_user_func($callback, $value);
}
}
return $value;
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Core;
class MiddlewarePipeline {
/** @var callable[] */
private $middlewares = [];
/**
* Add a middleware to the pipeline.
* @param callable $middleware Should return false to halt execution.
*/
public function add(callable $middleware): void {
$this->middlewares[] = $middleware;
}
/**
* Execute all middlewares in sequence.
* @return bool False if any middleware returns false, true otherwise.
*/
public function run(): bool {
foreach ($this->middlewares as $middleware) {
$result = call_user_func($middleware);
if ($result === false) {
return false;
}
}
return true;
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Core;
/**
* NullLogger is a fallback for disabling logging when there is no logging plugin enabled.
*/
class NullLogger
{
/**
* PSR-3 compatible log stub.
* @param string $level
* @param string $message
* @param array $context
*/
public function log(string $level, string $message, array $context = []): void {}
}

View File

@ -1,37 +0,0 @@
<?php
namespace App\Core;
class PluginManager
{
/**
* Loads all enabled plugins from the given directory.
*
* @param string $pluginsDir
* @return array<string, array{path: string, meta: array}>
*/
public static function load(string $pluginsDir): array
{
$enabled = [];
foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginPath) {
$manifest = $pluginPath . '/plugin.json';
if (!file_exists($manifest)) {
continue;
}
$meta = json_decode(file_get_contents($manifest), true);
if (empty($meta['enabled'])) {
continue;
}
$name = basename($pluginPath);
$enabled[$name] = [
'path' => $pluginPath,
'meta' => $meta,
];
$bootstrap = $pluginPath . '/bootstrap.php';
if (file_exists($bootstrap)) {
include_once $bootstrap;
}
}
return $enabled;
}
}

View File

@ -1,57 +0,0 @@
<?php
namespace App\Core;
use Session;
use Feedback;
class Router {
/**
* Check session validity and handle redirection for protected pages.
* Returns current username if session is valid, null otherwise.
*/
public static function checkAuth(array $config, string $app_root, array $public_pages, string $page): ?string {
// Always allow login page to be accessed
if ($page === 'login') {
return null;
}
// Check if this is a public page
$isPublicPage = in_array($page, $public_pages, true);
// For public pages, don't validate session
if ($isPublicPage) {
return null;
}
// For protected pages, check if we have a valid session
$validSession = Session::isValidSession(true);
// If session is valid, return the username
if ($validSession) {
return Session::getUsername();
}
// If we get here, we need to redirect to login
// Only show timeout message if we had an active session before
if (isset($_SESSION['LAST_ACTIVITY']) && !isset($_SESSION['session_timeout_shown'])) {
Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
$_SESSION['session_timeout_shown'] = true;
}
// Preserve flash messages
$flash_messages = $_SESSION['flash_messages'] ?? [];
Session::cleanup($config);
$_SESSION['flash_messages'] = $flash_messages;
// Build login URL with redirect if appropriate
$loginUrl = $app_root . '?page=login';
$trimmed = trim($page, '/?');
if (!empty($trimmed) && !in_array($trimmed, INVALID_REDIRECT_PAGES, true)) {
$loginUrl .= '&redirect=' . urlencode($_SERVER['REQUEST_URI']);
}
header('Location: ' . $loginUrl);
exit();
}
}

View File

@ -1,24 +0,0 @@
<?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'] ?? '';
}

View File

@ -1,15 +0,0 @@
<?php
/**
* Returns a logger instance: plugin Log if available, otherwise NullLogger.
*
* @param mixed $database Database or DatabaseConnector instance.
* @return mixed Logger instance with PSR-3 log() compatible method.
*/
function getLoggerInstance($database) {
if (class_exists('Log')) {
return new Log($database);
}
require_once __DIR__ . '/../core/NullLogger.php';
return new \App\Core\NullLogger();
}

View File

@ -0,0 +1,20 @@
<?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);
}

View File

@ -1,109 +0,0 @@
<?php
/**
* Theme Asset handler
*
* Serves theme assets (images, CSS, JS, etc.) securely by checking if the requested
* theme and asset path are valid and accessible.
*
* This is a standalone handler that doesn't require the full application initialization.
*/
// Set error reporting
error_reporting(E_ALL);
ini_set('display_errors', '1');
// Define base path if not defined
if (!defined('APP_ROOT')) {
define('APP_ROOT', dirname(__DIR__));
}
// Basic security checks
if (!isset($_GET['theme']) || !preg_match('/^[a-zA-Z0-9_-]+$/', $_GET['theme'])) {
http_response_code(400);
exit('Invalid theme specified');
}
if (!isset($_GET['path']) || empty($_GET['path'])) {
http_response_code(400);
exit('No asset path specified');
}
$themeId = $_GET['theme'];
$assetPath = $_GET['path'];
// Validate asset path (only alphanumeric, hyphen, underscore, dot, and forward slash)
if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) {
http_response_code(400);
exit('Invalid asset path');
}
// Prevent directory traversal
if (strpos($assetPath, '..') !== false) {
http_response_code(400);
exit('Invalid asset path');
}
// Build full path to the asset
$themesDir = dirname(dirname(__DIR__)) . '/themes';
$fullPath = realpath("$themesDir/$themeId/$assetPath");
// Additional security check to ensure the path is within the themes directory
if ($fullPath === false) {
http_response_code(404);
header('Content-Type: text/plain');
error_log("Asset not found: $themesDir/$themeId/$assetPath");
exit("Asset not found: $themesDir/$themeId/$assetPath");
}
if (strpos($fullPath, realpath($themesDir)) !== 0) {
http_response_code(400);
header('Content-Type: text/plain');
error_log("Security violation: Attempted to access path outside themes directory: $fullPath");
exit('Invalid asset path');
}
// Check if the file exists and is readable
if (!file_exists($fullPath) || !is_readable($fullPath)) {
http_response_code(404);
header('Content-Type: text/plain');
error_log("File not found or not readable: $fullPath");
exit("File not found or not readable: " . basename($fullPath));
}
// Clear any previous output
if (ob_get_level()) {
ob_clean();
}
// Determine content type based on file extension
$extension = strtolower(pathinfo($assetPath, PATHINFO_EXTENSION));
$contentTypes = [
'css' => 'text/css',
'js' => 'application/javascript',
'json' => 'application/json',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
'webp' => 'image/webp',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'ttf' => 'font/ttf',
'eot' => 'application/vnd.ms-fontobject',
];
$contentType = $contentTypes[$extension] ?? 'application/octet-stream';
// Set proper headers
header('Content-Type: ' . $contentType);
header('Content-Length: ' . filesize($fullPath));
// Cache for 24 hours (86400 seconds)
$expires = 86400;
header('Cache-Control: public, max-age=' . $expires);
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
header('Pragma: cache');
// Output the file
readfile($fullPath);

View File

@ -1,307 +0,0 @@
<?php
/**
* Theme Helper
*
* Handles theme management and template/asset loading for the application.
* Supports multiple themes with fallback to default theme when needed.
* The default theme uses app/templates and public_html/static as fallbacks/
*/
namespace App\Helpers;
use Exception;
// Include Session class
require_once __DIR__ . '/../classes/session.php';
use Session;
class Theme
{
/**
* @var array Theme configuration
*/
private static $config;
/**
* Get the theme configuration
*
* @return array
*/
public static function getConfig()
{
// Always reload the config to get the latest changes
self::$config = require __DIR__ . '/../config/theme.php';
return self::$config;
}
/**
* @var string Current theme name
*/
private static $currentTheme;
/**
* Initialize the theme system
*/
public static function init()
{
// Only load config if not already loaded
if (self::$config === null) {
self::$config = require __DIR__ . '/../config/theme.php';
}
self::$currentTheme = self::getCurrentThemeName();
}
/**
* Get the current theme name
*
* @return string
*/
public static function getCurrentThemeName()
{
// Ensure session is started
if (session_status() === PHP_SESSION_NONE) {
Session::startSession();
}
// Check if already determined
if (self::$currentTheme !== null) {
return self::$currentTheme;
}
// Try to get from session first
$sessionTheme = isset($_SESSION['theme']) ? $_SESSION['theme'] : null;
if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) {
self::$currentTheme = $sessionTheme;
} else {
// Fall back to default theme
self::$currentTheme = self::$config['active_theme'];
}
return self::$currentTheme;
}
/**
* Get the URL for a theme asset
*
* @param string $themeId Theme ID
* @param string $assetPath Path to the asset relative to theme directory (e.g., 'css/style.css')
* @return string|null URL to the asset or null if not found
*/
public static function getAssetUrl($themeId, $assetPath = '')
{
// Clean and validate the asset path
$assetPath = ltrim($assetPath, '/');
if (empty($assetPath)) {
return null;
}
// Only allow alphanumeric, hyphen, underscore, dot, and forward slash
if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) {
return null;
}
// Prevent directory traversal
if (strpos($assetPath, '..') !== false) {
return null;
}
$fullPath = __DIR__ . "/../../themes/$themeId/$assetPath";
if (!file_exists($fullPath) || !is_readable($fullPath)) {
return null;
}
// Generate URL that goes through index.php
global $app_root;
// Remove any trailing slash from app_root to avoid double slashes
$baseUrl = rtrim($app_root, '/');
return "$baseUrl/?page=theme-asset&theme=" . urlencode($themeId) . "&path=" . urlencode($assetPath);
}
/**
* Set the current theme for the session
*
* @param string $themeName
* @return bool
*/
public static function setCurrentTheme(string $themeName): bool
{
if (!self::themeExists($themeName)) {
return false;
}
// Update session
if (Session::isValidSession()) {
$_SESSION['theme'] = $themeName;
} else {
return false;
}
// Clear the current theme cache
self::$currentTheme = null;
// Update config file
$configFile = __DIR__ . '/../config/theme.php';
if (file_exists($configFile) && is_writable($configFile)) {
$config = file_get_contents($configFile);
// Update the active_theme in the config
$newConfig = preg_replace(
"/'active_theme'\s*=>\s*'[^']*'/",
"'active_theme' => '" . addslashes($themeName) . "'",
$config
);
if ($newConfig !== $config) {
if (file_put_contents($configFile, $newConfig) === false) {
return false;
}
}
self::$currentTheme = $themeName;
return true;
}
return false;
}
/**
* Check if a theme exists
*
* @param string $themeName
* @return bool
*/
public static function themeExists(string $themeName): bool
{
// Default theme always exists as it uses core templates
if ($themeName === 'default') {
return true;
}
$themePath = self::getThemePath($themeName);
return is_dir($themePath) && file_exists("$themePath/config.php");
}
/**
* Get the path to a theme
*
* @param string|null $themeName
* @return string
*/
public static function getThemePath(?string $themeName = null): string
{
$themeName = $themeName ?? self::getCurrentThemeName();
$config = self::getConfig();
return rtrim($config['paths']['themes'], '/') . "/$themeName";
}
/**
* Get the URL for a theme asset
*
* @param string $path
* @param bool $includeVersion
* @return string
*/
public static function asset($path, $includeVersion = false)
{
$themeName = self::getCurrentThemeName();
$config = self::getConfig();
$baseUrl = rtrim($GLOBALS['app_root'] ?? '', '/');
// For non-default themes, use theme assets
if ($themeName !== 'default') {
$assetPath = "/themes/{$themeName}/assets/" . ltrim($path, '/');
// Add version query string for cache busting
if ($includeVersion) {
$version = self::getThemeVersion($themeName);
$assetPath .= (strpos($assetPath, '?') !== false ? '&' : '?') . 'v=' . $version;
}
} else {
// For default theme, use public_html directly
$assetPath = '/' . ltrim($path, '/');
}
return $baseUrl . $assetPath;
}
/**
* Include a theme template file
*
* @param string $template Template name without .php extension
* @return void
*/
public static function include($template)
{
global $config;
$config = $config ?? [];
$themeConfig = self::getConfig();
$themeName = self::getCurrentThemeName();
// We need this here, otherwise because this helper
// between index and the views breaks the session vars
extract($GLOBALS, EXTR_SKIP | EXTR_REFS);
// Ensure config is always available in templates
$config = array_merge($config, $themeConfig);
// For non-default themes, look in the theme directory first
if ($themeName !== 'default') {
$themePath = $config['paths']['themes'] . '/' . $themeName . '/views/' . $template . '.php';
if (file_exists($themePath)) {
include $themePath;
return;
}
}
// Fallback to default template location
$defaultPath = $config['paths']['templates'] . '/' . $template . '.php';
if (file_exists($defaultPath)) {
include $defaultPath;
return;
}
// Log error if template not found
error_log("Template not found: {$template} in theme: {$themeName}");
}
/**
* Get all available themes
*
* @return array
*/
public static function getAvailableThemes(): array
{
$config = self::getConfig();
$availableThemes = $config['available_themes'] ?? [];
$themes = [];
// Add default theme if not already present
if (!isset($availableThemes['default'])) {
$availableThemes['default'] = 'Default built-in theme';
}
// Verify each theme exists and has a config file
$themesDir = $config['paths']['themes'] ?? (__DIR__ . '/../../themes');
foreach ($availableThemes as $id => $name) {
if ($id === 'default' || (is_dir("$themesDir/$id") && file_exists("$themesDir/$id/config.php"))) {
$themes[$id] = $name;
}
}
return $themes;
}
}
// Initialize the theme system
Theme::init();

View File

@ -1,5 +0,0 @@
<?php
// Pages that should not be used as redirect targets
const INVALID_REDIRECT_PAGES = [
'', 'login', 'logout', 'register', 'dashboard', '/'
];

View File

@ -1,9 +1,10 @@
<?php <?php
require_once __DIR__ . '/../helpers/security.php'; require_once __DIR__ . '/../helpers/security.php';
require_once __DIR__ . '/../helpers/logs.php';
function applyCsrfMiddleware() { function applyCsrfMiddleware() {
global $logObject, $user_IP; global $logObject;
$security = SecurityHelper::getInstance(); $security = SecurityHelper::getInstance();
// Skip CSRF check for GET requests // Skip CSRF check for GET requests
@ -33,14 +34,14 @@ function applyCsrfMiddleware() {
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!$security->verifyCsrfToken($token)) { if (!$security->verifyCsrfToken($token)) {
// Log CSRF attempt // Log CSRF attempt
$ipAddress = $user_IP; $ipAddress = getUserIP();
$logMessage = sprintf( $logMessage = sprintf(
"CSRF attempt detected - IP: %s, Page: %s, User: %s", "CSRF attempt detected - IP: %s, Page: %s, User: %s",
$ipAddress, $ipAddress,
$_GET['page'] ?? 'unknown', $_GET['page'] ?? 'unknown',
$_SESSION['username'] ?? 'anonymous' $_SESSION['username'] ?? 'anonymous'
); );
$logObject->log('error', $logMessage, ['user_id' => null, 'scope' => 'system']); $logObject->insertLog(null, $logMessage, 'system');
// Return error message // Return error message
http_response_code(403); http_response_code(403);

View File

@ -1,63 +1,61 @@
<?php <?php
// connect to database // connect to database
function connectDB($config) { function connectDB($config, $database = '', $dbFile = '', $platformId = '') {
// sqlite database file
if ($config['db_type'] === 'sqlite') { // connecting ti a jilo sqlite database
if ($database === 'jilo') {
try { try {
$dbFile = $config['sqlite']['sqlite_file'] ?? null;
if (!$dbFile || !file_exists($dbFile)) { if (!$dbFile || !file_exists($dbFile)) {
throw new Exception(getError("Database file \"{$dbFile}\"not found.")); throw new Exception(getError("Invalid platform ID \"{$platformId}\", database file \"{$dbFile}\" not found."));
} }
$db = new Database([ $db = new Database([
'type' => $config['db_type'], 'type' => 'sqlite',
'dbFile' => $dbFile, 'dbFile' => $dbFile,
]); ]);
$pdo = $db->getConnection(); return ['db' => $db, 'error' => null];
} catch (Exception $e) { } catch (Exception $e) {
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to DB.', $e->getMessage())); return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
return false;
} }
return $db;
// mysql/mariadb database // connecting to a jilo-web database of the web app
} elseif ($config['db_type'] === 'mysql' || $config['db_type'] === 'mariadb') {
$db = new Database([
'type' => $config['db_type'],
'host' => $config['sql']['sql_host'] ?? 'localhost',
'port' => $config['sql']['sql_port'] ?? '3306',
'dbname' => $config['sql']['sql_database'],
'user' => $config['sql']['sql_username'],
'password' => $config['sql']['sql_password'],
]);
try {
$pdo = $db->getConnection();
} catch (Exception $e) {
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to DB.', $e->getMessage()));
return false;
}
return $db;
// unknown database
} else { } else {
Feedback::flash('ERROR', 'DEFAULT', getError("Error: unknown database type \"{$config['db_type']}\""));
return false;
}
} // sqlite database file
if ($config['db']['db_type'] === 'sqlite') {
// connect to Jilo database try {
function connectJiloDB($config, $dbFile = '', $platformId = '') { $db = new Database([
try { 'type' => $config['db']['db_type'],
if (!$dbFile || !file_exists($dbFile)) { 'dbFile' => $config['db']['sqlite_file'],
throw new Exception(getError("Invalid platform ID \"{$platformId}\", database file \"{$dbFile}\" not found.")); ]);
$pdo = $db->getConnection();
return ['db' => $db, 'error' => null];
} catch (Exception $e) {
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
}
// mysql/mariadb database
} elseif ($config['db']['db_type'] === 'mysql' || $config['db']['db_type'] === 'mariadb') {
try {
$db = new Database([
'type' => $config['db']['db_type'],
'host' => $config['db']['sql_host'] ?? 'localhost',
'port' => $config['db']['sql_port'] ?? '3306',
'dbname' => $config['db']['sql_database'],
'user' => $config['db']['sql_username'],
'password' => $config['db']['sql_password'],
]);
$pdo = $db->getConnection();
return ['db' => $db, 'error' => null];
} catch (Exception $e) {
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
}
// unknown database
} else {
$error = "Error: unknow database type \"{$config['db']['db_type']}\"";
Feedback::flash('ERROR', 'DEFAULT', $error);
exit();
} }
$db = new Database([
'type' => 'sqlite',
'dbFile' => $dbFile,
]);
return ['db' => $db, 'error' => null];
} catch (Exception $e) {
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
require_once __DIR__ . '/../classes/ratelimiter.php'; require_once __DIR__ . '/../classes/ratelimiter.php';
require_once __DIR__ . '/../helpers/logs.php';
/** /**
* Rate limit middleware for page requests * Rate limit middleware for page requests
@ -12,10 +13,10 @@ require_once __DIR__ . '/../classes/ratelimiter.php';
* @return bool True if request is allowed, false if rate limited * @return bool True if request is allowed, false if rate limited
*/ */
function checkRateLimit($database, $endpoint, $userId = null, $existingRateLimiter = null) { function checkRateLimit($database, $endpoint, $userId = null, $existingRateLimiter = null) {
global $app_root, $user_IP; global $app_root;
$isTest = defined('PHPUNIT_RUNNING'); $isTest = defined('PHPUNIT_RUNNING');
$rateLimiter = $existingRateLimiter ?? new RateLimiter($database); $rateLimiter = $existingRateLimiter ?? new RateLimiter($database);
$ipAddress = $user_IP; $ipAddress = getUserIP();
// Check if request is allowed // Check if request is allowed
if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) { if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) {

View File

@ -0,0 +1,97 @@
<?php
/**
* Session Middleware
*
* Validates session status and handles session timeout.
* This middleware should be included in all protected pages.
*/
function applySessionMiddleware($config, $app_root) {
$isTest = defined('PHPUNIT_RUNNING');
// Access $_SESSION directly in test mode
if (!$isTest) {
// Start session if not already started
if (session_status() !== PHP_SESSION_ACTIVE && !headers_sent()) {
session_start([
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200 // 2 hours
]);
}
}
// Check if user is logged in with all required session variables
if (!isset($_SESSION['user_id']) || !isset($_SESSION['username'])) {
cleanupSession($config, $app_root, $isTest);
return false;
}
// Check session timeout
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 7200; // 30 days or 2 hours
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) {
// Session has expired
cleanupSession($config, $app_root, $isTest);
return false;
}
// Update last activity time
$_SESSION['LAST_ACTIVITY'] = time();
// Regenerate session ID periodically (every 30 minutes)
if (!isset($_SESSION['CREATED'])) {
$_SESSION['CREATED'] = time();
} else if (time() - $_SESSION['CREATED'] > 1800) {
// Regenerate session ID and update creation time
if (!$isTest && !headers_sent() && session_status() === PHP_SESSION_ACTIVE) {
$oldData = $_SESSION;
session_regenerate_id(true);
$_SESSION = $oldData;
$_SESSION['CREATED'] = time();
}
}
return true;
}
/**
* Helper function to clean up session data and redirect
*/
function cleanupSession($config, $app_root, $isTest) {
// Always clear session data
$_SESSION = array();
if (!$isTest) {
if (session_status() === PHP_SESSION_ACTIVE) {
session_unset();
session_destroy();
// Start a new session to prevent errors
if (!headers_sent()) {
session_start([
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 7200
]);
}
}
// Clear cookies
if (!headers_sent()) {
setcookie('username', '', [
'expires' => time() - 3600,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
}
header('Location: ' . $app_root . '?page=login&timeout=1');
exit();
}
}

View File

@ -11,16 +11,10 @@ return [
'LOGIN_SUCCESS' => 'Login successful.', 'LOGIN_SUCCESS' => 'Login successful.',
'LOGIN_FAILED' => 'Login failed. Please check your credentials.', 'LOGIN_FAILED' => 'Login failed. Please check your credentials.',
'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.', 'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.',
'SESSION_TIMEOUT' => 'Your session has expired. Please log in again.',
'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.', 'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.',
'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.', 'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.',
'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.', 'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.',
], ],
'REGISTER' => [
'SUCCESS' => 'Registration successful. You can log in now.',
'FAILED' => 'Registration failed: %s',
'DISABLED' => 'Registration is disabled.',
],
'SECURITY' => [ 'SECURITY' => [
'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.', 'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.',
'WHITELIST_ADD_FAILED' => 'Failed to add IP to whitelist.', 'WHITELIST_ADD_FAILED' => 'Failed to add IP to whitelist.',
@ -36,9 +30,10 @@ return [
'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.', 'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.',
'IP_REQUIRED' => 'IP address is required.', 'IP_REQUIRED' => 'IP address is required.',
], ],
'THEME' => [ 'REGISTER' => [
'THEME_CHANGE_SUCCESS' => 'Theme has been changed successfully.', 'SUCCESS' => 'Registration successful. You can log in now.',
'THEME_CHANGE_FAILED' => 'Failed to change theme. The selected theme may not be available.', 'FAILED' => 'Registration failed: %s',
'DISABLED' => 'Registration is disabled.',
], ],
'SYSTEM' => [ 'SYSTEM' => [
'DB_ERROR' => 'Error connecting to the database: %s', 'DB_ERROR' => 'Error connecting to the database: %s',

View File

@ -19,8 +19,8 @@ $agentId = filter_input(INPUT_GET, 'agent', FILTER_VALIDATE_INT);
require '../app/classes/agent.php'; require '../app/classes/agent.php';
require '../app/classes/host.php'; require '../app/classes/host.php';
$agentObject = new Agent($db); $agentObject = new Agent($dbWeb);
$hostObject = new Host($db); $hostObject = new Host($dbWeb);
/** /**
* Get the cache key for an agent * Get the cache key for an agent
@ -50,7 +50,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Apply rate limiting for adding new contacts // Apply rate limiting for adding new contacts
require '../app/includes/rate_limit_middleware.php'; require '../app/includes/rate_limit_middleware.php';
checkRateLimit($db, 'contact', $userId); checkRateLimit($dbWeb, 'contact', $user_id);
// Validate agent ID for POST operations // Validate agent ID for POST operations
if ($agentId === false || $agentId === null) { if ($agentId === false || $agentId === null) {

View File

@ -9,7 +9,7 @@
*/ */
// connect to database // connect to database
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
// if DB connection has error, display it and stop here // if DB connection has error, display it and stop here
if ($response['db'] === null) { if ($response['db'] === null) {

View File

@ -9,7 +9,7 @@
*/ */
// connect to database // connect to database
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
// if DB connection has error, display it and stop here // if DB connection has error, display it and stop here
if ($response['db'] === null) { if ($response['db'] === null) {

View File

@ -13,7 +13,8 @@ require '../app/classes/config.php';
require '../app/classes/api_response.php'; require '../app/classes/api_response.php';
// Initialize required objects // Initialize required objects
$userObject = new User($db); $userObject = new User($dbWeb);
$logObject = new Log($dbWeb);
$configObject = new Config(); $configObject = new Config();
// For AJAX requests // For AJAX requests
@ -50,8 +51,8 @@ if (!$isWritable) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Check if user has permission to edit config // Check if user has permission to edit config
if (!$userObject->hasRight($userId, 'edit config file')) { if (!$userObject->hasRight($user_id, 'edit config file')) {
$logObject->log('error', "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", ['user_id' => $userId, 'scope' => 'system']); $logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", 'system');
if ($isAjax) { if ($isAjax) {
ApiResponse::error('Forbidden: You do not have permission to edit the config file', null, 403); ApiResponse::error('Forbidden: You do not have permission to edit the config file', null, 403);
exit; exit;
@ -63,7 +64,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Apply rate limiting // Apply rate limiting
require '../app/includes/rate_limit_middleware.php'; require '../app/includes/rate_limit_middleware.php';
checkRateLimit($db, 'config', $userId); checkRateLimit($dbWeb, 'config', $user_id);
// Ensure no output before this point // Ensure no output before this point
ob_clean(); ob_clean();
@ -73,7 +74,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get raw input // Get raw input
$jsonData = file_get_contents('php://input'); $jsonData = file_get_contents('php://input');
if ($jsonData === false) { if ($jsonData === false) {
$logObject->log('error', "Failed to read request data for config update", ['user_id' => $userId, 'scope' => 'system']); $logObject->insertLog($user_id, "Failed to read request data for config update", 'system');
ApiResponse::error('Failed to read request data'); ApiResponse::error('Failed to read request data');
exit; exit;
} }
@ -114,11 +115,10 @@ if (!$isAjax) {
* Handles GET requests to display templates. * Handles GET requests to display templates.
*/ */
if ($userObject->hasRight($userId, 'superuser') || if ($userObject->hasRight($user_id, 'view config file')) {
$userObject->hasRight($userId, 'view config file')) {
include '../app/templates/config.php'; include '../app/templates/config.php';
} else { } else {
$logObject->log('error', "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", ['user_id' => $userId, 'scope' => 'system']); $logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", 'system');
include '../app/templates/error-unauthorized.php'; include '../app/templates/error-unauthorized.php';
} }
} }

View File

@ -14,10 +14,17 @@
* - `password`: Change password * - `password`: Change password
*/ */
// Initialize user object // Check if user is logged in
$userObject = new User($db); if (!isset($_SESSION['user_id'])) {
header("Location: $app_root?page=login");
exit();
}
$user_id = $_SESSION['user_id'];
// Initialize user object
$userObject = new User($dbWeb);
// Get action and item from request
$action = $_REQUEST['action'] ?? ''; $action = $_REQUEST['action'] ?? '';
$item = $_REQUEST['item'] ?? ''; $item = $_REQUEST['item'] ?? '';
@ -33,7 +40,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Apply rate limiting // Apply rate limiting
require_once '../app/includes/rate_limit_middleware.php'; require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($db, 'credentials', $userId); checkRateLimit($dbWeb, 'credentials', $user_id);
switch ($item) { switch ($item) {
case '2fa': case '2fa':
@ -43,7 +50,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$code = $_POST['code'] ?? ''; $code = $_POST['code'] ?? '';
$secret = $_POST['secret'] ?? ''; $secret = $_POST['secret'] ?? '';
if ($userObject->enableTwoFactor($userId, $secret, $code)) { if ($userObject->enableTwoFactor($user_id, $secret, $code)) {
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been enabled successfully.'); Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been enabled successfully.');
header("Location: $app_root?page=credentials"); header("Location: $app_root?page=credentials");
exit(); exit();
@ -60,7 +67,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
case 'verify': case 'verify':
// This is a user-initiated verification // This is a user-initiated verification
$code = $_POST['code'] ?? ''; $code = $_POST['code'] ?? '';
if ($userObject->verifyTwoFactor($userId, $code)) { if ($userObject->verifyTwoFactor($user_id, $code)) {
$_SESSION['2fa_verified'] = true; $_SESSION['2fa_verified'] = true;
header("Location: $app_root?page=dashboard"); header("Location: $app_root?page=dashboard");
exit(); exit();
@ -72,7 +79,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
break; break;
case 'disable': case 'disable':
if ($userObject->disableTwoFactor($userId)) { if ($userObject->disableTwoFactor($user_id)) {
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been disabled.'); Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been disabled.');
} else { } else {
Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable two-factor authentication.'); Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable two-factor authentication.');
@ -89,7 +96,8 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$validator = new Validator($_POST); $validator = new Validator($_POST);
$rules = [ $rules = [
'current_password' => [ 'current_password' => [
'required' => true 'required' => true,
'min' => 8
], ],
'new_password' => [ 'new_password' => [
'required' => true, 'required' => true,
@ -107,7 +115,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
exit(); exit();
} }
if ($userObject->changePassword($userId, $_POST['current_password'], $_POST['new_password'])) { if ($userObject->changePassword($user_id, $_POST['current_password'], $_POST['new_password'])) {
Feedback::flash('NOTICE', 'DEFAULT', 'Password has been changed successfully.'); Feedback::flash('NOTICE', 'DEFAULT', 'Password has been changed successfully.');
} else { } else {
Feedback::flash('ERROR', 'DEFAULT', 'Failed to change password. Please verify your current password.'); Feedback::flash('ERROR', 'DEFAULT', 'Failed to change password. Please verify your current password.');
@ -128,12 +136,12 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$security->generateCsrfToken(); $security->generateCsrfToken();
// Get 2FA status for the template // Get 2FA status for the template
$has2fa = $userObject->isTwoFactorEnabled($userId); $has2fa = $userObject->isTwoFactorEnabled($user_id);
switch ($action) { switch ($action) {
case 'setup': case 'setup':
if (!$has2fa) { if (!$has2fa) {
$result = $userObject->enableTwoFactor($userId); $result = $userObject->enableTwoFactor($user_id);
if ($result['success']) { if ($result['success']) {
$setupData = $result['data']; $setupData = $result['data'];
} else { } else {

View File

@ -16,7 +16,7 @@ require '../app/classes/conference.php';
require '../app/classes/participant.php'; require '../app/classes/participant.php';
// connect to database // connect to database
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
// if DB connection has error, display it and stop here // if DB connection has error, display it and stop here
if ($response['db'] === null) { if ($response['db'] === null) {

View File

@ -7,11 +7,11 @@ require '../app/classes/agent.php';
require '../app/classes/conference.php'; require '../app/classes/conference.php';
require '../app/classes/host.php'; require '../app/classes/host.php';
$agentObject = new Agent($db); $agentObject = new Agent($dbWeb);
$hostObject = new Host($db); $hostObject = new Host($dbWeb);
// Connect to Jilo database for log data // Connect to Jilo database for log data
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
if ($response['db'] === null) { if ($response['db'] === null) {
Feedback::flash('ERROR', 'DEFAULT', $response['error']); Feedback::flash('ERROR', 'DEFAULT', $response['error']);
} else { } else {

View File

@ -3,8 +3,8 @@
require '../app/classes/agent.php'; require '../app/classes/agent.php';
require '../app/classes/host.php'; require '../app/classes/host.php';
$agentObject = new Agent($db); $agentObject = new Agent($dbWeb);
$hostObject = new Host($db); $hostObject = new Host($dbWeb);
// Define metrics to display // Define metrics to display
$metrics = [ $metrics = [

View File

@ -19,13 +19,13 @@ unset($error);
try { try {
// connect to database // connect to database
$db = connectDB($config); $db = connectDB($config)['db'];
// Initialize RateLimiter // Initialize RateLimiter
require_once '../app/classes/ratelimiter.php'; require_once '../app/classes/ratelimiter.php';
$rateLimiter = new RateLimiter($db); $rateLimiter = new RateLimiter($db);
// Get user IP // Get user IP
require_once '../app/helpers/ip_helper.php';
$user_IP = getUserIP(); $user_IP = getUserIP();
$action = $_REQUEST['action'] ?? ''; $action = $_REQUEST['action'] ?? '';
@ -33,23 +33,21 @@ try {
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) { if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
// Handle 2FA verification // Handle 2FA verification
$code = $_POST['code'] ?? ''; $code = $_POST['code'] ?? '';
$pending2FA = Session::get2FAPending(); $userId = $_SESSION['2fa_pending_user_id'];
$username = $_SESSION['2fa_pending_username'];
if (!$pending2FA) { $rememberMe = isset($_SESSION['2fa_pending_remember']);
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
exit();
}
require_once '../app/classes/twoFactorAuth.php'; require_once '../app/classes/twoFactorAuth.php';
$twoFactorAuth = new TwoFactorAuthentication($db); $twoFactorAuth = new TwoFactorAuthentication($db);
if ($twoFactorAuth->verify($pending2FA['user_id'], $code)) { if ($twoFactorAuth->verify($userId, $code)) {
// Complete login // Complete login
handleSuccessfulLogin($pending2FA['user_id'], $pending2FA['username'], handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $user_IP);
$pending2FA['remember_me'], $config, $app_root, $logObject, $user_IP);
// Clean up 2FA session data // Clean up 2FA session data
Session::clear2FAPending(); unset($_SESSION['2fa_pending_user_id']);
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
exit(); exit();
} }
@ -62,9 +60,6 @@ try {
// Get any new feedback messages // Get any new feedback messages
include '../app/helpers/feedback.php'; include '../app/helpers/feedback.php';
// Make userId available to template
$userId = $pending2FA['user_id'];
// Load the 2FA verification template // Load the 2FA verification template
include '../app/templates/credentials-2fa-verify.php'; include '../app/templates/credentials-2fa-verify.php';
exit(); exit();
@ -97,7 +92,7 @@ try {
// Process reset request // Process reset request
require_once '../app/classes/passwordReset.php'; require_once '../app/classes/passwordReset.php';
$resetHandler = new PasswordReset($db, $config); $resetHandler = new PasswordReset($db);
$result = $resetHandler->requestReset($email); $result = $resetHandler->requestReset($email);
// Always show same message whether email exists or not for security // Always show same message whether email exists or not for security
@ -123,7 +118,7 @@ try {
// Handle password reset // Handle password reset
try { try {
require_once '../app/classes/passwordReset.php'; require_once '../app/classes/passwordReset.php';
$resetHandler = new PasswordReset($db, $config); $resetHandler = new PasswordReset($db);
$token = $_GET['token']; $token = $_GET['token'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@ -201,7 +196,8 @@ try {
], ],
'password' => [ 'password' => [
'type' => 'string', 'type' => 'string',
'required' => true 'required' => true,
'min' => 5
] ]
]; ];
@ -224,6 +220,9 @@ try {
if ($rateLimiter->tooManyAttempts($username, $user_IP)) { if ($rateLimiter->tooManyAttempts($username, $user_IP)) {
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']); throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
} }
// Record this attempt
$rateLimiter->attempt($username, $user_IP);
} }
// Attempt login // Attempt login
@ -233,8 +232,11 @@ try {
switch ($loginResult['status']) { switch ($loginResult['status']) {
case 'requires_2fa': case 'requires_2fa':
// Store pending 2FA info // Store pending 2FA info
Session::store2FAPending($loginResult['user_id'], $loginResult['username'], $_SESSION['2fa_pending_user_id'] = $loginResult['user_id'];
isset($formData['remember_me'])); $_SESSION['2fa_pending_username'] = $loginResult['username'];
if (isset($formData['remember_me'])) {
$_SESSION['2fa_pending_remember'] = true;
}
// Redirect to 2FA verification // Redirect to 2FA verification
header('Location: ?page=login&action=verify'); header('Location: ?page=login&action=verify');
@ -243,7 +245,7 @@ try {
case 'success': case 'success':
// Complete login // Complete login
handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'], handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'],
isset($formData['remember_me']), $config, $app_root, $logObject, $user_IP); isset($formData['remember_me']), $config, $logObject, $user_IP);
exit(); exit();
default: default:
@ -256,9 +258,8 @@ try {
// Log the failed attempt // Log the failed attempt
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
if (isset($username)) { if (isset($username)) {
$userId = $userObject->getUserId($username)[0]['id'] ?? 0; $user_id = $userObject->getUserId($username)[0]['id'] ?? 0;
$logObject->log('error', "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", ['user_id' => $userId, 'scope' => 'user']); $logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user');
$rateLimiter->attempt($username, $user_IP);
} }
} }
} }
@ -268,7 +269,7 @@ try {
// Show configured login message if any // Show configured login message if any
if (!empty($config['login_message'])) { if (!empty($config['login_message'])) {
echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false); echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false);
} }
// Get any new feedback messages // Get any new feedback messages
@ -280,26 +281,42 @@ include '../app/templates/form-login.php';
/** /**
* Handle successful login by setting up session and cookies * Handle successful login by setting up session and cookies
*/ */
function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $app_root, $logObject, $userIP) { function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $userIP) {
// Create authenticated session if ($rememberMe) {
Session::createAuthSession($userId, $username, $rememberMe, $config); // 30*24*60*60 = 30 days
$cookie_lifetime = 30 * 24 * 60 * 60;
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
} else {
// 0 - session end on browser close
$cookie_lifetime = 0;
$setcookie_lifetime = 0;
}
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// set session lifetime and cookies
setcookie('username', $username, [
'expires' => $setcookie_lifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Set session variables
$_SESSION['user_id'] = $userId;
$_SESSION['USERNAME'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
if ($rememberMe) {
$_SESSION['REMEMBER_ME'] = true;
}
// Log successful login // Log successful login
$logObject->log('info', "Login: User \"$username\" logged in. IP: $userIP", ['user_id' => $userId, 'scope' => 'user']); $logObject->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user');
// Set success message // Set success message and redirect
Feedback::flash('LOGIN', 'LOGIN_SUCCESS'); Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
header('Location: ' . htmlspecialchars($app_root));
// After successful login, redirect to original page if provided in URL param or POST
$redirect = $app_root;
$candidate = $_POST['redirect'] ?? $_GET['redirect'] ?? '';
$trimmed = trim($candidate, '/?');
if (
(strpos($candidate, '/') === 0 || strpos($candidate, '?') === 0)
&& !in_array($trimmed, INVALID_REDIRECT_PAGES, true)
) {
$redirect = $candidate;
}
header('Location: ' . htmlspecialchars($redirect));
exit();
} }

View File

@ -8,17 +8,12 @@
* It supports pagination and filtering. * It supports pagination and filtering.
*/ */
// Define plugin base path if not already defined // Get any new feedback messages
if (!defined('PLUGIN_LOGS_PATH')) { include '../app/helpers/feedback.php';
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 // Check for rights; user or system
$has_system_access = ($userObject->hasRight($userId, 'superuser') || $has_system_access = ($userObject->hasRight($user_id, 'superuser') ||
$userObject->hasRight($userId, 'view app logs')); $userObject->hasRight($user_id, 'view app logs'));
// Get current page for pagination // Get current page for pagination
$currentPage = $_REQUEST['page_num'] ?? 1; $currentPage = $_REQUEST['page_num'] ?? 1;
@ -74,8 +69,8 @@ if (isset($_REQUEST['tab'])) {
} }
// prepare the result // prepare the result
$search = $logObject->readLog($userId, $scope, $offset, $items_per_page, $filters); $search = $logObject->readLog($user_id, $scope, $offset, $items_per_page, $filters);
$search_all = $logObject->readLog($userId, $scope, 0, 0, $filters); $search_all = $logObject->readLog($user_id, $scope, 0, 0, $filters);
if (!empty($search)) { if (!empty($search)) {
// we get total items and number of pages // we get total items and number of pages
@ -91,7 +86,6 @@ if (!empty($search)) {
$log_record = array( $log_record = array(
// assign title to the field in the array record // assign title to the field in the array record
'time' => $item['time'], 'time' => $item['time'],
'log level' => $item['level'],
'log message' => $item['message'] 'log message' => $item['message']
); );
} else { } else {
@ -100,7 +94,6 @@ if (!empty($search)) {
'userID' => $item['user_id'], 'userID' => $item['user_id'],
'username' => $item['username'], 'username' => $item['username'],
'time' => $item['time'], 'time' => $item['time'],
'log level' => $item['level'],
'log message' => $item['message'] 'log message' => $item['message']
); );
} }
@ -110,13 +103,7 @@ if (!empty($search)) {
} }
} }
$username = $userObject->getUserDetails($userId)[0]['username']; $username = $userObject->getUserDetails($user_id)[0]['username'];
// Get any new feedback messages // Load the template
include dirname(__FILE__, 4) . '/app/helpers/feedback.php'; include '../app/templates/logs.php';
// Load plugin helpers
include PLUGIN_LOGS_PATH . 'helpers/logs_view_helper.php';
// Display messages list
include PLUGIN_LOGS_PATH . 'views/logs.php';

View File

@ -9,7 +9,7 @@
*/ */
// connect to database // connect to database
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
// if DB connection has error, display it and stop here // if DB connection has error, display it and stop here
if ($response['db'] === null) { if ($response['db'] === null) {

View File

@ -30,11 +30,11 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Apply rate limiting for profile operations // Apply rate limiting for profile operations
require_once '../app/includes/rate_limit_middleware.php'; require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($db, 'profile', $userId); checkRateLimit($dbWeb, 'profile', $user_id);
// avatar removal // avatar removal
if ($item === 'avatar' && $action === 'remove') { if ($item === 'avatar' && $action === 'remove') {
$validator = new Validator(['user_id' => $userId]); $validator = new Validator(['user_id' => $user_id]);
$rules = [ $rules = [
'user_id' => [ 'user_id' => [
'required' => true, 'required' => true,
@ -48,7 +48,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
exit(); exit();
} }
$result = $userObject->removeAvatar($userId, $config['avatars_path'].$userDetails[0]['avatar']); $result = $userObject->removeAvatar($user_id, $config['avatars_path'].$userDetails[0]['avatar']);
if ($result === true) { if ($result === true) {
Feedback::flash('NOTICE', 'DEFAULT', "Avatar for user \"{$userDetails[0]['username']}\" is removed."); Feedback::flash('NOTICE', 'DEFAULT', "Avatar for user \"{$userDetails[0]['username']}\" is removed.");
} else { } else {
@ -89,54 +89,50 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
'timezone' => htmlspecialchars($_POST['timezone'] ?? ''), 'timezone' => htmlspecialchars($_POST['timezone'] ?? ''),
'bio' => htmlspecialchars($_POST['bio'] ?? ''), 'bio' => htmlspecialchars($_POST['bio'] ?? ''),
]; ];
$result = $userObject->editUser($userId, $updatedUser); $result = $userObject->editUser($user_id, $updatedUser);
if ($result === true) { if ($result === true) {
Feedback::flash('NOTICE', 'DEFAULT', "User details for \"{$userDetails[0]['username']}\" are edited."); Feedback::flash('NOTICE', 'DEFAULT', "User details for \"{$updatedUser['name']}\" are edited.");
} else { } else {
Feedback::flash('ERROR', 'DEFAULT', "Editing the user details failed. Error: $result"); Feedback::flash('ERROR', 'DEFAULT', "Editing the user details failed. Error: $result");
} }
// update the rights // update the rights
// Get current rights IDs if (isset($_POST['rights'])) {
$userRightsIds = array_column($userRights, 'right_id'); $validator = new Validator(['rights' => $_POST['rights']]);
$rules = [
'rights' => [
'array' => true
]
];
// If no rights are selected, remove all rights if (!$validator->validate($rules)) {
if (!isset($_POST['rights'])) { Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
$_POST['rights'] = []; header("Location: $app_root?page=profile");
} exit();
$validator = new Validator(['rights' => $_POST['rights']]);
$rules = [
'rights' => [
'array' => true
]
];
if (!$validator->validate($rules)) {
Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
header("Location: $app_root?page=profile");
exit();
}
$newRights = $_POST['rights'];
// what rights we need to add
$rightsToAdd = array_diff($newRights, $userRightsIds);
if (!empty($rightsToAdd)) {
foreach ($rightsToAdd as $rightId) {
$userObject->addUserRight($userId, $rightId);
} }
}
// what rights we need to remove $newRights = $_POST['rights'];
$rightsToRemove = array_diff($userRightsIds, $newRights); // extract the new right_ids
if (!empty($rightsToRemove)) { $userRightsIds = array_column($userRights, 'right_id');
foreach ($rightsToRemove as $rightId) { // what rights we need to add
$userObject->removeUserRight($userId, $rightId); $rightsToAdd = array_diff($newRights, $userRightsIds);
if (!empty($rightsToAdd)) {
foreach ($rightsToAdd as $rightId) {
$userObject->addUserRight($user_id, $rightId);
}
}
// what rights we need to remove
$rightsToRemove = array_diff($userRightsIds, $newRights);
if (!empty($rightsToRemove)) {
foreach ($rightsToRemove as $rightId) {
$userObject->removeUserRight($user_id, $rightId);
}
} }
} }
// update the avatar // update the avatar
if (!empty($_FILES['avatar_file']['tmp_name'])) { if (!empty($_FILES['avatar_file']['tmp_name'])) {
$result = $userObject->changeAvatar($userId, $_FILES['avatar_file'], $config['avatars_path']); $result = $userObject->changeAvatar($user_id, $_FILES['avatar_file'], $config['avatars_path']);
} }
header("Location: $app_root?page=profile"); header("Location: $app_root?page=profile");

View File

@ -8,31 +8,24 @@
* and redirects to the login page on success or displays an error message on failure. * and redirects to the login page on success or displays an error message on failure.
*/ */
// Define plugin base path if not already defined
if (!defined('PLUGIN_REGISTER_PATH')) {
define('PLUGIN_REGISTER_PATH', dirname(__FILE__, 2) . '/');
}
require_once PLUGIN_REGISTER_PATH . 'models/register.php';
require_once dirname(__FILE__, 4) . '/app/classes/user.php';
require_once dirname(__FILE__, 4) . '/app/classes/validator.php';
require_once dirname(__FILE__, 4) . '/app/helpers/security.php';
// registration is allowed, go on // registration is allowed, go on
if ($config['registration_enabled'] == true) { if ($config['registration_enabled'] == true) {
try { try {
global $db, $logObject, $userObject; global $dbWeb, $logObject, $userObject;
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) { if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
// Apply rate limiting // Apply rate limiting
require_once dirname(__FILE__, 4) . '/app/includes/rate_limit_middleware.php'; require '../app/includes/rate_limit_middleware.php';
checkRateLimit($db, 'register'); checkRateLimit($dbWeb, 'register');
require_once '../app/classes/validator.php';
require_once '../app/helpers/security.php';
$security = SecurityHelper::getInstance(); $security = SecurityHelper::getInstance();
// Sanitize input // Sanitize input
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'confirm_password', 'csrf_token', 'terms']); $formData = $security->sanitizeArray($_POST, ['username', 'password', 'confirm_password', 'csrf_token']);
// Validate CSRF token // Validate CSRF token
if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) { if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) {
@ -54,10 +47,6 @@ if ($config['registration_enabled'] == true) {
'confirm_password' => [ 'confirm_password' => [
'required' => true, 'required' => true,
'matches' => 'password' 'matches' => 'password'
],
'terms' => [
'required' => true,
'equals' => 'on'
] ]
]; ];
@ -67,42 +56,41 @@ if ($config['registration_enabled'] == true) {
$password = $formData['password']; $password = $formData['password'];
// registering // registering
$register = new Register($db); $result = $userObject->register($username, $password);
$result = $register->register($username, $password);
// redirect to login // redirect to login
if ($result === true) { if ($result === true) {
// Get the new user's ID for logging // Get the new user's ID for logging
$userId = $userObject->getUserId($username)[0]['id']; $user_id = $userObject->getUserId($username)[0]['id'];
$logObject->log('info', "Registration: New user \"$username\" registered successfully. IP: $user_IP", ['user_id' => $userId, 'scope' => 'user']); $logObject->insertLog($user_id, "Registration: New user \"$username\" registered successfully. IP: $user_IP", 'user');
Feedback::flash('NOTICE', 'DEFAULT', "Registration successful. You can log in now."); Feedback::flash('NOTICE', 'DEFAULT', "Registration successful. You can log in now.");
header('Location: ' . htmlspecialchars($app_root . '?page=login')); header('Location: ' . htmlspecialchars($app_root));
exit(); exit();
// registration fail, redirect to login // registration fail, redirect to login
} else { } else {
$logObject->log('error', "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", ['user_id' => null, 'scope' => 'system']); $logObject->insertLog(0, "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", 'system');
Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result"); Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result");
header('Location: ' . htmlspecialchars($app_root . '?page=register')); header('Location: ' . htmlspecialchars($app_root));
exit(); exit();
} }
} else { } else {
$error = $validator->getFirstError(); $error = $validator->getFirstError();
$logObject->log('error', "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", ['user_id' => null, 'scope' => 'system']); $logObject->insertLog(0, "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", 'system');
Feedback::flash('ERROR', 'DEFAULT', $error); Feedback::flash('ERROR', 'DEFAULT', $error);
header('Location: ' . htmlspecialchars($app_root . '?page=register')); header('Location: ' . htmlspecialchars($app_root . '?page=register'));
exit(); exit();
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
$logObject->log('error', "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), ['user_id' => null, 'scope' => 'system']); $logObject->insertLog(0, "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), 'system');
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
} }
// Get any new feedback messages // Get any new feedback messages
include dirname(__FILE__, 4) . '/app/helpers/feedback.php'; include '../app/helpers/feedback.php';
// Load the template // Load the template
include PLUGIN_REGISTER_PATH . 'views/form-register.php'; include '../app/templates/form-register.php';
// registration disabled // registration disabled
} else { } else {

View File

@ -1,10 +1,15 @@
<?php <?php
// Check if user has any of the required rights // Check if user has any of the required rights
if (!($userObject->hasRight($userId, 'superuser') || if (!($userObject->hasRight($user_id, 'superuser') ||
$userObject->hasRight($userId, 'edit whitelist') || $userObject->hasRight($user_id, 'edit whitelist') ||
$userObject->hasRight($userId, 'edit blacklist') || $userObject->hasRight($user_id, 'edit blacklist') ||
$userObject->hasRight($userId, 'edit ratelimiting'))) { $userObject->hasRight($user_id, 'edit ratelimiting'))) {
include '../app/templates/error-unauthorized.php';
exit;
}
if (!isset($currentUser)) {
include '../app/templates/error-unauthorized.php'; include '../app/templates/error-unauthorized.php';
exit; exit;
} }
@ -14,7 +19,7 @@ $section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section'
// Initialize RateLimiter // Initialize RateLimiter
require_once '../app/classes/ratelimiter.php'; require_once '../app/classes/ratelimiter.php';
$rateLimiter = new RateLimiter($db); $rateLimiter = new RateLimiter($dbWeb);
// Handle form submissions // Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
@ -22,7 +27,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
// Apply rate limiting for security operations // Apply rate limiting for security operations
require_once '../app/includes/rate_limit_middleware.php'; require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($db, 'security', $userId); checkRateLimit($dbWeb, 'security', $user_id);
$action = $_POST['action']; $action = $_POST['action'];
$validator = new Validator($_POST); $validator = new Validator($_POST);
@ -30,7 +35,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
try { try {
switch ($action) { switch ($action) {
case 'add_whitelist': case 'add_whitelist':
if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) { if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -49,7 +54,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($validator->validate($rules)) { if ($validator->validate($rules)) {
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on'; $is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $userId)) { if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $user_id)) {
Feedback::flash('SECURITY', 'WHITELIST_ADD_FAILED'); Feedback::flash('SECURITY', 'WHITELIST_ADD_FAILED');
} else { } else {
Feedback::flash('SECURITY', 'WHITELIST_ADD_SUCCESS'); Feedback::flash('SECURITY', 'WHITELIST_ADD_SUCCESS');
@ -60,7 +65,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
break; break;
case 'remove_whitelist': case 'remove_whitelist':
if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) { if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -74,7 +79,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
]; ];
if ($validator->validate($rules)) { if ($validator->validate($rules)) {
if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $userId)) { if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $user_id)) {
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED'); Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED');
} else { } else {
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS'); Feedback::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS');
@ -85,7 +90,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
break; break;
case 'add_blacklist': case 'add_blacklist':
if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) { if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -111,7 +116,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on'; $is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
$expiry_hours = !empty($_POST['expiry_hours']) ? (int)$_POST['expiry_hours'] : null; $expiry_hours = !empty($_POST['expiry_hours']) ? (int)$_POST['expiry_hours'] : null;
if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'], $currentUser, $userId, $expiry_hours)) { if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'], $currentUser, $user_id, $expiry_hours)) {
Feedback::flash('SECURITY', 'BLACKLIST_ADD_FAILED'); Feedback::flash('SECURITY', 'BLACKLIST_ADD_FAILED');
} else { } else {
Feedback::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS'); Feedback::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS');
@ -122,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
break; break;
case 'remove_blacklist': case 'remove_blacklist':
if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) { if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -136,7 +141,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
]; ];
if ($validator->validate($rules)) { if ($validator->validate($rules)) {
if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $userId)) { if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $user_id)) {
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED'); Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED');
} else { } else {
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS'); Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS');

View File

@ -21,8 +21,8 @@ $host = $_REQUEST['host'] ?? '';
require '../app/classes/host.php'; require '../app/classes/host.php';
require '../app/classes/agent.php'; require '../app/classes/agent.php';
$hostObject = new Host($db); $hostObject = new Host($dbWeb);
$agentObject = new Agent($db); $agentObject = new Agent($dbWeb);
if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_SERVER['REQUEST_METHOD'] == 'POST') {
/** /**
@ -31,7 +31,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Apply rate limiting for profile operations // Apply rate limiting for profile operations
require_once '../app/includes/rate_limit_middleware.php'; require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($db, 'profile', $userId); checkRateLimit($dbWeb, 'profile', $user_id);
// Get hash from URL if present // Get hash from URL if present
$hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? ''; $hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? '';
@ -170,7 +170,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
* Handles GET requests to display templates. * Handles GET requests to display templates.
*/ */
if ($userObject->hasRight($userId, 'view settings') || $userObject->hasRight($userId, 'superuser')) { if ($userObject->hasRight($user_id, 'view settings')) {
$jilo_agent_types = $agentObject->getAgentTypes(); $jilo_agent_types = $agentObject->getAgentTypes();
include '../app/templates/settings.php'; include '../app/templates/settings.php';
} else { } else {

View File

@ -13,8 +13,8 @@ include '../app/helpers/feedback.php';
require '../app/classes/agent.php'; require '../app/classes/agent.php';
require '../app/classes/host.php'; require '../app/classes/host.php';
$agentObject = new Agent($db); $agentObject = new Agent($dbWeb);
$hostObject = new Host($db); $hostObject = new Host($dbWeb);
include '../app/templates/status-server.php'; include '../app/templates/status-server.php';
@ -22,7 +22,7 @@ include '../app/templates/status-server.php';
foreach ($platformsAll as $platform) { foreach ($platformsAll as $platform) {
// check if we can connect to the jilo database // check if we can connect to the jilo database
$response = connectJiloDB($config, $platform['jilo_database'], $platform['id']); $response = connectDB($config, 'jilo', $platform['jilo_database'], $platform['id']);
if ($response['error'] !== null) { if ($response['error'] !== null) {
$jilo_database_status = $response['error']; $jilo_database_status = $response['error'];
} else { } else {

View File

@ -1,10 +0,0 @@
<?php
/**
* Theme Asset handler
*
* Serves theme assets through the main application router.
* This provides a secure way to serve theme files that are outside the web root.
*/
// Include the theme asset handler
require_once __DIR__ . '/../helpers/theme-asset.php';

View File

@ -1,71 +0,0 @@
<?php
/**
* Theme Management Controller
*
* Handles theme switching and management functionality.
* Allows users to view available themes and change the active theme.
*
* Actions:
* - switch_to: Changes the active theme for the current user
*/
// Initialize security
require_once '../app/helpers/security.php';
$security = SecurityHelper::getInstance();
// Only allow access to logged-in users
if (!Session::isValidSession()) {
header('Location: ' . $app_root . '?page=login');
exit;
}
// Handle theme switching
if (isset($_GET['switch_to'])) {
$themeName = $_GET['switch_to'];
// Validate CSRF token for state-changing operations
if (!$security->verifyCsrfToken($_GET['csrf_token'] ?? '')) {
Feedback::flash('SECURITY', 'CSRF_INVALID');
header("Location: $app_root?page=theme");
exit();
}
if (\App\Helpers\Theme::setCurrentTheme($themeName)) {
// Set success message
Feedback::flash('THEME', 'THEME_CHANGED');
} else {
// Set error message
Feedback::flash('THEME', 'THEME_CHANGE_FAILED');
}
// Redirect back to prevent form resubmission
$redirect = $app_root . '?page=theme';
header("Location: $redirect");
exit;
}
// Get available themes and current theme for the view
$themes = \App\Helpers\Theme::getAvailableThemes();
$currentTheme = \App\Helpers\Theme::getCurrentThemeName();
// Prepare theme data with screenshot URLs for the view
$themeData = [];
foreach ($themes as $id => $name) {
$themeData[$id] = [
'name' => $name,
'screenshotUrl' => \App\Helpers\Theme::getAssetUrl($id, 'screenshot.png'),
'isActive' => $id === $currentTheme
];
}
// Make theme data available to the view
$themes = $themeData;
// Generate CSRF token for the form
$csrf_token = $security->generateCsrfToken();
// Get any new feedback messages
include '../app/helpers/feedback.php';
// Load the template
include '../app/templates/theme.php';

View File

@ -17,8 +17,7 @@
<i class="fas fa-wrench me-2 text-secondary"></i> <i class="fas fa-wrench me-2 text-secondary"></i>
<?= htmlspecialchars($config['site_name']) ?> app configuration <?= htmlspecialchars($config['site_name']) ?> app configuration
</h5> </h5>
<?php if ($userObject->hasRight($userId, 'superuser') || <?php if ($userObject->hasRight($user_id, 'edit config file')) { ?>
$userObject->hasRight($userId, 'edit config file')) { ?>
<div> <div>
<button type="button" class="btn btn-outline-primary btn-sm toggle-edit" <?= !$isWritable ? 'disabled' : '' ?>> <button type="button" class="btn btn-outline-primary btn-sm toggle-edit" <?= !$isWritable ? 'disabled' : '' ?>>
<i class="fas fa-edit me-2"></i>Edit <i class="fas fa-edit me-2"></i>Edit
@ -38,7 +37,7 @@
<div class="card-body p-4"> <div class="card-body p-4">
<form id="configForm"> <form id="configForm">
<?php <?php
include CSRF_TOKEN_INCLUDE; include 'csrf_token.php';
function renderConfigItem($key, $value, $path = '') { function renderConfigItem($key, $value, $path = '') {
$fullPath = $path ? $path . '[' . $key . ']' : $key; $fullPath = $path ? $path . '[' . $key . ']' : $key;

View File

@ -4,15 +4,15 @@
<div class="card-body"> <div class="card-body">
<p class="card-text"><strong>Welcome to <?= htmlspecialchars($config['site_name']); ?>!</strong><br />Please enter login credentials:</p> <p class="card-text"><strong>Welcome to <?= htmlspecialchars($config['site_name']); ?>!</strong><br />Please enter login credentials:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login"> <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login">
<?php include CSRF_TOKEN_INCLUDE; ?> <?php include 'csrf_token.php'; ?>
<div class="form-group mb-3"> <div class="form-group mb-3">
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username" <input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _" pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
required /> required autofocus />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password" <input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
pattern=".{8,}" title="Eight or more characters" pattern=".{5,}" title="Eight or more characters"
required /> required />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
@ -21,9 +21,6 @@
remember me remember me
</label> </label>
</div> </div>
<?php if (isset($_GET['redirect'])): ?>
<input type="hidden" name="redirect" value="<?php echo htmlspecialchars($_GET['redirect']); ?>">
<?php endif; ?>
<input type="submit" class="btn btn-primary" value="Login" /> <input type="submit" class="btn btn-primary" value="Login" />
</form> </form>
<div class="mt-3"> <div class="mt-3">

View File

@ -8,7 +8,7 @@
<p>Enter your email address and we will send you<br /> <p>Enter your email address and we will send you<br />
instructions to reset your password.</p> instructions to reset your password.</p>
<form method="post" action="?page=login&action=forgot"> <form method="post" action="?page=login&action=forgot">
<?php include CSRF_TOKEN_INCLUDE; ?> <?php include 'csrf_token.php'; ?>
<div class="form-group"> <div class="form-group">
<label for="email">email address:</label> <label for="email">email address:</label>
<input type="email" <input type="email"

View File

@ -6,7 +6,7 @@
<div class="card-body"> <div class="card-body">
<h3 class="card-title mb-4">Set new password</h3> <h3 class="card-title mb-4">Set new password</h3>
<form method="post" action="?page=login&action=reset&token=<?= htmlspecialchars(urlencode($token)) ?>"> <form method="post" action="?page=login&action=reset&token=<?= htmlspecialchars(urlencode($token)) ?>">
<?php include CSRF_TOKEN_INCLUDE; ?> <?php include 'csrf_token.php'; ?>
<div class="form-group"> <div class="form-group">
<label for="new_password">new password:</label> <label for="new_password">new password:</label>
<input type="password" <input type="password"

View File

@ -4,11 +4,11 @@
<div class="card-body"> <div class="card-body">
<p class="card-text">Enter credentials for registration:</p> <p class="card-text">Enter credentials for registration:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=register"> <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=register">
<?php include CSRF_TOKEN_INCLUDE; ?> <?php include 'csrf_token.php'; ?>
<div class="form-group mb-3"> <div class="form-group mb-3">
<input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username" <input type="text" class="form-control w-50 mx-auto" name="username" placeholder="Username"
pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _" pattern="[A-Za-z0-9_\-]{3,20}" title="3-20 characters, letters, numbers, - and _"
required /> required autofocus />
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password" <input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
@ -20,17 +20,6 @@
pattern=".{8,}" title="Eight or more characters" pattern=".{8,}" title="Eight or more characters"
required /> required />
</div> </div>
<div class="form-group mb-3">
<div class="form-check">
<label class="form-check-label" for="terms">
<input type="checkbox" class="form-check-input" id="terms" name="terms" required>
I agree to the <a href="<?= htmlspecialchars($app_root) ?>?page=terms" target="_blank">terms & conditions</a> and <a href="<?= htmlspecialchars($app_root) ?>?page=privacy" target="_blank">privacy policy</a>
</label>
</div>
<small class="text-muted mt-2">
We use cookies to improve your experience. See our <a href="<?= htmlspecialchars($app_root) ?>?page=cookies" target="_blank">cookies policy</a>
</small>
</div>
<input type="submit" class="btn btn-primary" value="Register" /> <input type="submit" class="btn btn-primary" value="Register" />
</form> </form>
</div> </div>

View File

@ -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 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 -->

View File

@ -12,7 +12,7 @@
</div> </div>
<?php if (Session::getUsername() && $page !== 'logout') { ?> <?php if (isset($currentUser) && $page !== 'logout') { ?>
<script src="static/js/sidebar.js"></script> <script src="static/js/sidebar.js"></script>
<?php } ?> <?php } ?>

View File

@ -10,7 +10,7 @@
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css"> <link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css">
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/messages.css"> <link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/messages.css">
<script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script> <script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script>
<?php if (Session::getUsername()) { ?> <?php if (isset($currentUser)) { ?>
<script> <script>
// restore sidebar state before the page is rendered // restore sidebar state before the page is rendered
(function () { (function () {

View File

@ -15,10 +15,10 @@
</div> </div>
<li class="font-weight-light text-uppercase" style="font-size: 0.5em; color: whitesmoke; margin-right: 70px; align-content: center;"> <li class="font-weight-light text-uppercase" style="font-size: 0.5em; color: whitesmoke; margin-right: 70px; align-content: center;">
version&nbsp;<?= htmlspecialchars($config['version'] ?? '1.0.0') ?> version&nbsp;<?= htmlspecialchars($config['version']) ?>
</li> </li>
<?php if (Session::isValidSession()) { ?> <?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?>
<?php foreach ($platformsAll as $platform) { <?php foreach ($platformsAll as $platform) {
$platform_switch_url = switchPlatform($platform['id']); $platform_switch_url = switchPlatform($platform['id']);
@ -40,17 +40,13 @@
</ul> </ul>
<ul class="menu-right"> <ul class="menu-right">
<?php if (Session::isValidSession()) { ?> <?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?>
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
</a> </a>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<h6 class="dropdown-header"><?= htmlspecialchars($currentUser) ?></h6> <h6 class="dropdown-header"><?= htmlspecialchars($currentUser) ?></h6>
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=theme">
<i class="fas fa-paint-brush"></i>Change theme
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=profile"> <a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=profile">
<i class="fas fa-id-card"></i>Profile details <i class="fas fa-id-card"></i>Profile details
</a> </a>
@ -63,49 +59,10 @@
</a> </a>
</div> </div>
</li> </li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i class="fas fa-cog"></i>
</a>
<div class="dropdown-menu dropdown-menu-right">
<h6 class="dropdown-header">system</h6>
<?php if ($userObject->hasRight($userId, 'superuser') ||
$userObject->hasRight($userId, 'view config file')) {?>
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=config">
<i class="fas fa-wrench"></i>Configuration
</a>
<?php } ?>
<?php if ($userObject->hasRight($userId, 'superuser') ||
$userObject->hasRight($userId, 'view config file') ||
$userObject->hasRight($userId, 'edit config file') ||
$userObject->hasRight($userId, 'edit whitelist') ||
$userObject->hasRight($userId, 'edit blacklist') ||
$userObject->hasRight($userId, 'edit ratelimiting')) { ?>
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=security">
<i class="fas fa-shield-alt"></i>Security
</a>
<?php } ?>
<?php if ($userObject->hasRight($userId, 'view app logs')) {?>
<?php do_hook('main_menu', ['app_root' => $app_root, 'section' => 'main', 'position' => 100]); ?>
<?php } ?>
</div>
</li>
<?php } else { ?> <?php } else { ?>
<li><a href="<?= htmlspecialchars($app_root) ?>?page=login">login</a></li> <li><a href="<?= htmlspecialchars($app_root) ?>?page=login">login</a></li>
<?php do_hook('main_public_menu', ['app_root' => $app_root, 'section' => 'main', 'position' => 100]); ?> <li><a href="<?= htmlspecialchars($app_root) ?>?page=register">register</a></li>
<?php } ?> <?php } ?>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i class="fas fa-info-circle"></i>
</a>
<div class="dropdown-menu dropdown-menu-right">
<h6 class="dropdown-header">resources</h6>
<a class="dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=help">
<i class="fas fa-question-circle"></i>Help
</a>
</div>
</li>
</ul> </ul>
</div> </div>
<!-- /Menu --> <!-- /Menu -->

View File

@ -72,11 +72,45 @@ $timeNow = new DateTime('now', new DateTimeZone($userTimezone));
<i class="fas fa-cog" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="jilo settings"></i>settings <i class="fas fa-cog" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="jilo settings"></i>settings
</li> </li>
</a> </a>
<li class="list-group-item bg-light" style="border: none;"><p class="text-end mb-0"><small>system</small></p></li>
<?php if ($userObject->hasRight($user_id, 'view config file')) {?>
<a href="<?= htmlspecialchars($app_root) ?>?page=config">
<li class="list-group-item<?php if ($page === 'config') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-wrench" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="app config"></i>config
</li>
</a>
<?php } ?>
<?php if ($userObject->hasRight($user_id, 'superuser') ||
$userObject->hasRight($user_id, 'edit whitelist') ||
$userObject->hasRight($user_id, 'edit blacklist') ||
$userObject->hasRight($user_id, 'edit ratelimiting')) { ?>
<a href="<?= htmlspecialchars($app_root) ?>?page=security">
<li class="list-group-item<?php if ($page === 'security') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-shield-alt" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="security"></i>security
</li>
</a>
<?php } ?>
<a href="<?= htmlspecialchars($app_root) ?>?page=status"> <a href="<?= htmlspecialchars($app_root) ?>?page=status">
<li class="list-group-item<?php if ($page === 'status' && $item === '') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>"> <li class="list-group-item<?php if ($page === 'status' && $item === '') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-heartbeat" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="status"></i>status <i class="fas fa-heartbeat" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="status"></i>status
</li> </li>
</a> </a>
<?php if ($userObject->hasRight($user_id, 'view app logs')) {?>
<a href="<?= htmlspecialchars($app_root) ?>?page=logs">
<li class="list-group-item<?php if ($page === 'logs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-shoe-prints" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="logs"></i>logs
</li>
</a>
<?php } ?>
<a href="<?= htmlspecialchars($app_root) ?>?page=help">
<li class="list-group-item<?php if ($page === 'help') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-question-circle" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="help"></i>help
</li>
</a>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -6,14 +6,11 @@
* $totalPages - Total number of pages * $totalPages - Total number of pages
*/ */
// Validate required pagination variables // Ensure required variables are set
if (!isset($currentPage) || !isset($totalPages)) { if (!isset($currentPage) || !isset($totalPages)) {
return; return;
} }
// Ensure valid values
$currentPage = max(1, min($currentPage, $totalPages));
// Number of page links to show before and after current page // Number of page links to show before and after current page
$range = 2; $range = 2;
?> ?>

View File

@ -6,7 +6,6 @@
<div class="card-body"> <div class="card-body">
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile" enctype="multipart/form-data"> <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile" enctype="multipart/form-data">
<?php include CSRF_TOKEN_INCLUDE; ?>
<div class="row"> <div class="row">
<p class="border rounded bg-light mb-4"><small>edit the profile fields</small></p> <p class="border rounded bg-light mb-4"><small>edit the profile fields</small></p>
<div class="col-md-4 avatar-container"> <div class="col-md-4 avatar-container">
@ -133,7 +132,6 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<form id="remove-avatar-form" data-action="remove-avatar" method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile&action=remove&item=avatar"> <form id="remove-avatar-form" data-action="remove-avatar" method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile&action=remove&item=avatar">
<?php include CSRF_TOKEN_INCLUDE; ?>
<button type="button" class="btn btn-danger" id="confirm-delete">Delete Avatar</button> <button type="button" class="btn btn-danger" id="confirm-delete">Delete Avatar</button>
</form> </form>
</div> </div>

View File

@ -5,17 +5,17 @@
<h2 class="mb-0">Security settings</h2> <h2 class="mb-0">Security settings</h2>
<small>network restrictions to control flooding and brute force attacks</small> <small>network restrictions to control flooding and brute force attacks</small>
<ul class="nav nav-tabs mt-5"> <ul class="nav nav-tabs mt-5">
<?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit whitelist')) { ?> <?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist')) { ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $section === 'whitelist' ? 'active' : '' ?>" href="?page=security&section=whitelist">IP whitelist</a> <a class="nav-link <?= $section === 'whitelist' ? 'active' : '' ?>" href="?page=security&section=whitelist">IP whitelist</a>
</li> </li>
<?php } ?> <?php } ?>
<?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit blacklist')) { ?> <?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist')) { ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $section === 'blacklist' ? 'active' : '' ?>" href="?page=security&section=blacklist">IP blacklist</a> <a class="nav-link <?= $section === 'blacklist' ? 'active' : '' ?>" href="?page=security&section=blacklist">IP blacklist</a>
</li> </li>
<?php } ?> <?php } ?>
<?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit ratelimiting')) { ?> <?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting')) { ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $section === 'ratelimit' ? 'active' : '' ?>" href="?page=security&section=ratelimit">Rate limiting</a> <a class="nav-link <?= $section === 'ratelimit' ? 'active' : '' ?>" href="?page=security&section=ratelimit">Rate limiting</a>
</li> </li>
@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<?php if ($section === 'whitelist' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit whitelist'))) { ?> <?php if ($section === 'whitelist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist'))) { ?>
<!-- whitelist section --> <!-- whitelist section -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
@ -35,7 +35,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" class="mb-4"> <form method="POST" class="mb-4">
<?php include CSRF_TOKEN_INCLUDE; ?> <?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="add_whitelist"> <input type="hidden" name="action" value="add_whitelist">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-4">
@ -77,7 +77,7 @@
<td><?= htmlspecialchars($ip['created_at']) ?></td> <td><?= htmlspecialchars($ip['created_at']) ?></td>
<td> <td>
<form method="POST" style="display: inline;"> <form method="POST" style="display: inline;">
<?php include CSRF_TOKEN_INCLUDE; ?> <?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="remove_whitelist"> <input type="hidden" name="action" value="remove_whitelist">
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>"> <input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from whitelist?')">Remove</button> <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from whitelist?')">Remove</button>
@ -93,7 +93,7 @@
</div> </div>
<?php } ?> <?php } ?>
<?php if ($section === 'blacklist' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit blacklist'))) { ?> <?php if ($section === 'blacklist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist'))) { ?>
<!-- blacklist section --> <!-- blacklist section -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
@ -104,7 +104,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" class="mb-4"> <form method="POST" class="mb-4">
<?php include CSRF_TOKEN_INCLUDE; ?> <?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="add_blacklist"> <input type="hidden" name="action" value="add_blacklist">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-3"> <div class="col-md-3">
@ -151,7 +151,7 @@
<td><?= $ip['expiry_time'] ? htmlspecialchars($ip['expiry_time']) : 'Never' ?></td> <td><?= $ip['expiry_time'] ? htmlspecialchars($ip['expiry_time']) : 'Never' ?></td>
<td> <td>
<form method="POST" style="display: inline;"> <form method="POST" style="display: inline;">
<?php include CSRF_TOKEN_INCLUDE; ?> <?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="remove_blacklist"> <input type="hidden" name="action" value="remove_blacklist">
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>"> <input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from blacklist?')">Remove</button> <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from blacklist?')">Remove</button>
@ -167,7 +167,7 @@
</div> </div>
<?php } ?> <?php } ?>
<?php if ($section === 'ratelimit' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, 'edit ratelimiting'))) { ?> <?php if ($section === 'ratelimit' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting'))) { ?>
<!-- rate limiting section --> <!-- rate limiting section -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">

View File

@ -57,7 +57,7 @@
<button type="button" class="btn btn-outline-secondary cancel-edit platform-edit-mode" style="display: none;"> <button type="button" class="btn btn-outline-secondary cancel-edit platform-edit-mode" style="display: none;">
<i class="fas fa-times me-1"></i>Cancel <i class="fas fa-times me-1"></i>Cancel
</button> </button>
<?php if ($userObject->hasRight($userId, 'delete platform')): ?> <?php if ($userObject->hasRight($user_id, 'delete platform')): ?>
<button type="button" class="btn btn-outline-danger platform-view-mode" onclick="showDeletePlatformModal(<?= htmlspecialchars($platform['id']) ?>, '<?= htmlspecialchars(addslashes($platform['name'])) ?>', '<?= htmlspecialchars(addslashes($platform['jitsi_url'])) ?>', '<?= htmlspecialchars(addslashes($platform['jilo_database'])) ?>')"> <button type="button" class="btn btn-outline-danger platform-view-mode" onclick="showDeletePlatformModal(<?= htmlspecialchars($platform['id']) ?>, '<?= htmlspecialchars(addslashes($platform['name'])) ?>', '<?= htmlspecialchars(addslashes($platform['jitsi_url'])) ?>', '<?= htmlspecialchars(addslashes($platform['jilo_database'])) ?>')">
<i class="fas fa-trash me-1"></i>Delete platform <i class="fas fa-trash me-1"></i>Delete platform
</button> </button>

View File

@ -1,44 +0,0 @@
<?php
/**
* Theme switcher template
*
* Displays available themes and allows the user to switch between them.
*
* @var array $themes List of available themes with their data
* - name: Display name
* - screenshotUrl: URL to the screenshot (or null if not available)
* - isActive: Whether this is the current theme
*/
?>
<div class="container mt-4">
<h2>Theme switcher</h2>
<p class="text-muted">Select a theme to change the appearance of the application.</p>
<div class="row mt-4">
<?php foreach ($themes as $themeId => $theme): ?>
<div class="col-md-4 mb-4">
<div class="card h-100 <?= $theme['isActive'] ? 'border-primary' : '' ?>">
<!-- Theme screenshot -->
<div class="theme-screenshot" style="height: 150px; background-size: cover; background-position: center; background-color: #f8f9fa; <?= $theme['screenshotUrl'] ? 'background-image: url(' . htmlspecialchars($theme['screenshotUrl']) . ')' : '' ?>">
<?php if (!$theme['screenshotUrl']): ?>
<div class="h-100 d-flex align-items-center justify-content-center text-muted">No preview available</div>
<?php endif; ?>
</div>
<?php if ($theme['isActive']): ?>
<div class="card-header bg-primary text-white">Current theme</div>
<?php endif; ?>
<div class="card-body d-flex flex-column">
<h5 class="card-title"><?= htmlspecialchars($theme['name']) ?></h5>
<p class="card-text text-muted">Theme ID: <code><?= htmlspecialchars($themeId) ?></code></p>
<div class="mt-auto">
<?php if (!$theme['isActive']): ?>
<a href="?page=theme&switch_to=<?= urlencode($themeId) ?>&csrf_token=<?= $csrf_token ?>" class="btn btn-primary">Switch to this theme</a>
<?php else: ?>
<button class="btn btn-outline-secondary" disabled>Currently active</button>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>

View File

@ -1,243 +0,0 @@
-- Time: 13.03.2025, 15:52
-- Server: 11.4.5-MariaDB-1
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
SET NAMES utf8mb4;
--
-- User profiles
--
-- --------------------------------------------------------
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
INSERT INTO `user` (`id`, `username`, `password`) VALUES
(1,'demo','$2y$12$AtIKs3eVxD4wTT1IWwJujuuHyGhhmfBJYqSfIrPFFPMDfKu3Rcsx6'),
(2,'demo1','$2y$12$ELwYyhQ8XDkVvX9Xsb0mlORqeQHNFaBOvaBuPQym4n4IomA/DgvLC');
-- --------------------------------------------------------
CREATE TABLE `user_meta` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`email` varchar(256) DEFAULT NULL,
`timezone` varchar(255) DEFAULT NULL,
`avatar` varchar(255) DEFAULT NULL,
`bio` text DEFAULT NULL,
PRIMARY KEY (`id`,`user_id`) USING BTREE,
KEY `user_id` (`user_id`),
CONSTRAINT `user_meta_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
INSERT INTO `user_meta` (`id`, `user_id`, `name`, `email`, `timezone`, `avatar`, `bio`) VALUES
(1,1,'demo admin user','admin@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web'),
(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
-- --------------------------------------------------------
CREATE TABLE `right` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
INSERT INTO `right` (`id`, `name`) VALUES
(1, 'superuser'),
(2, 'edit users'),
(3, 'view config file'),
(4, 'edit config file'),
(5, 'view own profile'),
(6, 'edit own profile'),
(7, 'view all profiles'),
(8, 'edit all profiles'),
(9, 'view app logs'),
(10, 'manage plugins'),
(11,'view all platforms'),
(12,'edit all platforms'),
(13,'view all agents'),
(14,'edit all agents'),
(15,'view jilo config');
-- --------------------------------------------------------
CREATE TABLE `user_right` (
`user_id` int(11) NOT NULL,
`right_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`right_id`),
KEY `fk_right_id` (`right_id`),
CONSTRAINT `fk_right_id` FOREIGN KEY (`right_id`) REFERENCES `right` (`id`),
CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
-- --------------------------------------------------------
CREATE TABLE `user_2fa` (
`user_id` int(11) NOT NULL,
`secret_key` varchar(64) NOT NULL,
`backup_codes` text,
`enabled` tinyint(1) NOT NULL DEFAULT 0,
`created_at` datetime NOT NULL,
`last_used` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`),
CONSTRAINT `fk_user_2fa_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
-- --------------------------------------------------------
CREATE TABLE `user_2fa_temp` (
`user_id` int(11) NOT NULL,
`code` varchar(6) NOT NULL,
`created_at` datetime NOT NULL,
`expires_at` datetime NOT NULL,
PRIMARY KEY (`user_id`, `code`),
CONSTRAINT `fk_user_2fa_temp_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
-- --------------------------------------------------------
CREATE TABLE `user_password_reset` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` int(11) NOT NULL,
`token` varchar(64) NOT NULL,
`expires` int(11) NOT NULL,
`used` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT `fk_user_password_reset` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
UNIQUE KEY `token_idx` (`token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
--
-- Login security
--
-- --------------------------------------------------------
CREATE TABLE `security_rate_auth` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip_address` varchar(45) NOT NULL,
`username` varchar(255) NOT NULL,
`attempted_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_ip_username` (`ip_address`,`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
-- --------------------------------------------------------
CREATE TABLE `security_rate_page` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip_address` varchar(45) NOT NULL,
`endpoint` varchar(255) NOT NULL,
`request_time` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_ip_endpoint` (`ip_address`,`endpoint`),
KEY `idx_request_time` (`request_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
-- --------------------------------------------------------
CREATE TABLE `security_ip_blacklist` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip_address` varchar(45) NOT NULL,
`is_network` tinyint(1) DEFAULT 0,
`reason` varchar(255) DEFAULT NULL,
`expiry_time` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
`created_by` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_ip` (`ip_address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
INSERT INTO `security_ip_blacklist` (`id`, `ip_address`, `is_network`, `reason`, `expiry_time`, `created_at`, `created_by`) VALUES
(1, '0.0.0.0/8', 1, 'Reserved address space - RFC 1122', NULL, '2025-01-03 16:40:15', 'system'),
(2, '100.64.0.0/10', 1, 'Carrier-grade NAT space - RFC 6598', NULL, '2025-01-03 16:40:15', 'system'),
(3, '192.0.2.0/24', 1, 'TEST-NET-1 Documentation space - RFC 5737', NULL, '2025-01-03 16:40:15', 'system'),
(4, '198.51.100.0/24', 1, 'TEST-NET-2 Documentation space - RFC 5737', NULL, '2025-01-03 16:40:15', 'system'),
(5, '203.0.113.0/24', 1, 'TEST-NET-3 Documentation space - RFC 5737', NULL, '2025-01-03 16:40:15', 'system');
-- --------------------------------------------------------
CREATE TABLE `security_ip_whitelist` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip_address` varchar(45) NOT NULL,
`is_network` tinyint(1) DEFAULT 0,
`description` varchar(255) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
`created_by` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_ip` (`ip_address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
INSERT INTO `security_ip_whitelist` (`id`, `ip_address`, `is_network`, `description`, `created_at`, `created_by`) VALUES
(1, '127.0.0.1', 0, 'localhost IPv4', '2025-01-03 16:40:15', 'system'),
(2, '::1', 0, 'localhost IPv6', '2025-01-03 16:40:15', 'system'),
(3, '10.0.0.0/8', 1, 'Private network (Class A)', '2025-01-03 16:40:15', 'system'),
(4, '172.16.0.0/12', 1, 'Private network (Class B)', '2025-01-03 16:40:15', 'system'),
(5, '192.168.0.0/16', 1, 'Private network (Class C)', '2025-01-03 16:40:15', 'system');
--
-- Jilo
--
-- --------------------------------------------------------
CREATE TABLE `jilo_agent_type` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`description` varchar(255),
`endpoint` varchar(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
INSERT INTO `jilo_agent_type` (`id`, `description`, `endpoint`) VALUES
(1,'jvb','/jvb'),
(2,'jicofo','/jicofo'),
(3,'prosody','/prosody'),
(4,'nginx','/nginx'),
(5,'jibri','/jibri');
-- --------------------------------------------------------
CREATE TABLE `platform` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`jitsi_url` varchar(255) NOT NULL,
`jilo_database` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
INSERT INTO `platforms` (`id`, `name`, `jitsi_url`, `jilo_database`) VALUES
(1,'example.com','https://meet.example.com','../../jilo/jilo.db');
-- --------------------------------------------------------
CREATE TABLE `host` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`address` varchar(255) NOT NULL,
`platform_id` int(11) NOT NULL,
`name` varchar(255),
PRIMARY KEY (`id`),
CONSTRAINT `host_ibfk_1` FOREIGN KEY (`platform_id`) REFERENCES `platform` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
-- --------------------------------------------------------
CREATE TABLE `jilo_agent` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`host_id` int(11) NOT NULL,
`agent_type_id` int(11) NOT NULL,
`url` varchar(255) NOT NULL,
`secret_key` varchar(255),
`check_period` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
CONSTRAINT `jilo_agent_ibfk_1` FOREIGN KEY (`agent_type_id`) REFERENCES `jilo_agent_type` (`id`),
CONSTRAINT `jilo_agent_ibfk_2` FOREIGN KEY (`host_id`) REFERENCES `host` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
-- --------------------------------------------------------
CREATE TABLE `jilo_agent_check` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`agent_id` int(11),
`timestamp` datetime DEFAULT current_timestamp(),
`status_code` int(11),
`response_time_ms` int(11),
`response_content` varchar(255),
PRIMARY KEY (`id`),
CONSTRAINT `jilo_agent_check_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `jilo_agent` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
COMMIT;

View File

@ -1,3 +1,4 @@
CREATE TABLE users ( CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
@ -24,95 +25,43 @@ CREATE TABLE rights (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE name TEXT NOT NULL UNIQUE
); );
CREATE TABLE IF NOT EXISTS "jilo_agent_types" ( INSERT INTO rights VALUES(1,'superuser');
id INTEGER PRIMARY KEY AUTOINCREMENT, INSERT INTO rights VALUES(2,'edit users');
description TEXT, INSERT INTO rights VALUES(3,'view settings');
endpoint TEXT INSERT INTO rights VALUES(4,'edit settings');
); INSERT INTO rights VALUES(5,'view config file');
INSERT INTO rights VALUES(6,'edit config file');
INSERT INTO rights VALUES(7,'view own profile');
INSERT INTO rights VALUES(8,'edit own profile');
INSERT INTO rights VALUES(9,'view all profiles');
INSERT INTO rights VALUES(10,'edit all profiles');
INSERT INTO rights VALUES(11,'view app logs');
INSERT INTO rights VALUES(12,'view all platforms');
INSERT INTO rights VALUES(13,'edit all platforms');
INSERT INTO rights VALUES(14,'view all agents');
INSERT INTO rights VALUES(15,'edit all agents');
CREATE TABLE platforms ( CREATE TABLE platforms (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
jitsi_url TEXT NOT NULL, jitsi_url TEXT NOT NULL,
jilo_database TEXT NOT NULL jilo_database TEXT NOT NULL
); );
CREATE TABLE hosts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
address TEXT NOT NULL,
platform_id INTEGER NOT NULL,
name TEXT,
FOREIGN KEY(platform_id) REFERENCES platforms(id)
);
CREATE TABLE jilo_agents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL,
agent_type_id INTEGER NOT NULL,
url TEXT NOT NULL,
secret_key TEXT,
check_period INTEGER DEFAULT 0,
FOREIGN KEY(agent_type_id) REFERENCES jilo_agent_types(id),
FOREIGN KEY(host_id) REFERENCES hosts(id)
);
CREATE TABLE jilo_agent_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
status_code INTEGER,
response_time_ms INTEGER,
response_content TEXT,
FOREIGN KEY(agent_id) REFERENCES jilo_agents(id)
);
CREATE TABLE ip_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description TEXT,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
);
CREATE TABLE ip_blacklist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT,
expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
);
CREATE TABLE logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
time TEXT DEFAULT (DATETIME('now')),
scope TEXT NOT NULL,
message TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE pages_rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
endpoint TEXT NOT NULL,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now'))
);
CREATE TABLE user_2fa ( CREATE TABLE user_2fa (
user_id INTEGER NOT NULL PRIMARY KEY, user_id INTEGER NOT NULL PRIMARY KEY,
secret_key TEXT NOT NULL, secret_key TEXT NOT NULL,
backup_codes TEXT, backup_codes TEXT,
enabled INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
last_used TEXT, last_used TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) FOREIGN KEY (user_id) REFERENCES users(id)
); );
CREATE TABLE user_2fa_temp ( CREATE TABLE user_2fa_temp (
user_id INTEGER NOT NULL PRIMARY KEY, user_id INTEGER NOT NULL,
code TEXT NOT NULL, code TEXT NOT NULL,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
expires_at TEXT NOT NULL, expires_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) PRIMARY KEY (user_id, code),
FOREIGN KEY (user_id) REFERENCES users(id)
); );
CREATE TABLE user_password_reset ( CREATE TABLE user_password_reset (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -121,5 +70,85 @@ CREATE TABLE user_password_reset (
expires INTEGER NOT NULL, expires INTEGER NOT NULL,
used INTEGER NOT NULL DEFAULT 0, used INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
); );
CREATE TABLE logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
time TEXT DEFAULT (DATETIME('now')),
scope TEXT NOT NULL,
message TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS "jilo_agent_types" (
"id" INTEGER,
"description" TEXT,
"endpoint" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);
INSERT INTO jilo_agent_types VALUES(1,'jvb','/jvb');
INSERT INTO jilo_agent_types VALUES(2,'jicofo','/jicofo');
INSERT INTO jilo_agent_types VALUES(3,'prosody','/prosody');
INSERT INTO jilo_agent_types VALUES(4,'nginx','/nginx');
INSERT INTO jilo_agent_types VALUES(5,'jibri','/jibri');
CREATE TABLE jilo_agent_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
status_code INTEGER,
response_time_ms INTEGER,
response_content TEXT,
FOREIGN KEY(agent_id) REFERENCES jilo_agents(id)
);
CREATE TABLE IF NOT EXISTS "jilo_agents" (
"id" INTEGER,
"host_id" INTEGER NOT NULL,
"agent_type_id" INTEGER NOT NULL,
"url" TEXT NOT NULL,
"secret_key" TEXT,
"check_period" INTEGER DEFAULT 0,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("agent_type_id") REFERENCES "jilo_agent_types"("id"),
FOREIGN KEY("host_id") REFERENCES "hosts"("id")
);
CREATE TABLE IF NOT EXISTS "hosts" (
"id" INTEGER NOT NULL,
"address" TEXT NOT NULL,
"platform_id" INTEGER NOT NULL,
"name" TEXT,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("platform_id") REFERENCES "platforms"("id")
);
CREATE TABLE login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now'))
);
CREATE TABLE ip_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description TEXT,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
);
INSERT INTO ip_whitelist VALUES(1,'127.0.0.1',0,'localhost IPv4','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(2,'::1',0,'localhost IPv6','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(3,'10.0.0.0/8',1,'Private network (Class A)','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(4,'172.16.0.0/12',1,'Private network (Class B)','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(5,'192.168.0.0/16',1,'Private network (Class C)','2025-01-04 11:39:08','system');
CREATE TABLE ip_blacklist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT,
expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
);
INSERT INTO ip_blacklist VALUES(1,'0.0.0.0/8',1,'Reserved address space - RFC 1122',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(2,'100.64.0.0/10',1,'Carrier-grade NAT space - RFC 6598',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(3,'192.0.2.0/24',1,'TEST-NET-1 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(4,'198.51.100.0/24',1,'TEST-NET-2 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(5,'203.0.113.0/24',1,'TEST-NET-3 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');

View File

@ -5,38 +5,24 @@ INSERT INTO users VALUES(2,'demo1','$2y$10$LtV9m.rMCJ.K/g45e6tzDexZ8C/9xxu3qFCkv
INSERT INTO users_meta VALUES(1,1,'demo admin user','admin@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web'); INSERT INTO users_meta VALUES(1,1,'demo admin user','admin@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
INSERT INTO users_meta VALUES(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web'); INSERT INTO users_meta VALUES(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
INSERT INTO rights VALUES(1,'superuser'); INSERT INTO platforms VALUES(1,'meet.lindeas.com','https://meet.lindeas.com','../jilo-meet.lindeas.db');
INSERT INTO rights VALUES(2,'edit users'); INSERT INTO platforms VALUES(2,'example.com','https://meet.example.com','../jilo.db');
INSERT INTO rights VALUES(3,'view settings');
INSERT INTO rights VALUES(4,'edit settings');
INSERT INTO rights VALUES(5,'view own profile');
INSERT INTO rights VALUES(6,'edit own profile');
INSERT INTO rights VALUES(7,'view all profiles');
INSERT INTO rights VALUES(8,'edit all profiles');
INSERT INTO rights VALUES(9,'view app logs');
INSERT INTO rights VALUES(10,'manage plugins');
INSERT INTO rights VALUES(11,'view all platforms');
INSERT INTO rights VALUES(12,'edit all platforms');
INSERT INTO rights VALUES(13,'view all agents');
INSERT INTO rights VALUES(14,'edit all agents');
INSERT INTO rights VALUES(15,'view jilo config');
INSERT INTO jilo_agent_types VALUES(1,'jvb','/jvb'); INSERT INTO logs VALUES(1,2,'2024-09-30 09:54:50','user','Logout: User "demo" logged out. IP: 151.237.101.43');
INSERT INTO jilo_agent_types VALUES(2,'jicofo','/jicofo'); INSERT INTO logs VALUES(2,2,'2024-09-30 09:54:54','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO jilo_agent_types VALUES(3,'prosody','/prosody'); INSERT INTO logs VALUES(3,2,'2024-10-03 16:34:49','user','Logout: User "demo" logged out. IP: 151.237.101.43');
INSERT INTO jilo_agent_types VALUES(4,'nginx','/nginx'); INSERT INTO logs VALUES(4,2,'2024-10-03 16:34:56','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO jilo_agent_types VALUES(5,'jibri','/jibri'); INSERT INTO logs VALUES(5,2,'2024-10-09 11:08:16','user','Logout: User "demo" logged out. IP: 151.237.101.43');
INSERT INTO logs VALUES(6,2,'2024-10-09 11:08:20','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO logs VALUES(7,2,'2024-10-17 16:22:57','user','Logout: User "demo" logged out. IP: 151.237.101.43');
INSERT INTO logs VALUES(8,2,'2024-10-17 16:23:08','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO logs VALUES(9,2,'2024-10-18 08:07:25','user','Login: User "demo" logged in. IP: 42.104.201.119');
INSERT INTO platforms VALUES(1,'example.com','https://meet.example.com','../../jilo/jilo.db'); INSERT INTO jilo_agents VALUES(1,1,1,'https://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO jilo_agents VALUES(4,1,2,'https://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO jilo_agents VALUES(7,1,3,'http://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO jilo_agents VALUES(8,1,4,'http://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO ip_whitelist VALUES(1,'127.0.0.1',0,'localhost IPv4','2025-01-04 11:39:08','system'); INSERT INTO hosts VALUES(1,'meet.lindeas.com',2,'main machine');
INSERT INTO ip_whitelist VALUES(2,'::1',0,'localhost IPv6','2025-01-04 11:39:08','system'); INSERT INTO hosts VALUES(2,'meet.example.com',2,'test');
INSERT INTO ip_whitelist VALUES(3,'10.0.0.0/8',1,'Private network (Class A)','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(4,'172.16.0.0/12',1,'Private network (Class B)','2025-01-04 11:39:08','system');
INSERT INTO ip_whitelist VALUES(5,'192.168.0.0/16',1,'Private network (Class C)','2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(1,'0.0.0.0/8',1,'Reserved address space - RFC 1122',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(2,'100.64.0.0/10',1,'Carrier-grade NAT space - RFC 6598',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(3,'192.0.2.0/24',1,'TEST-NET-1 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(4,'198.51.100.0/24',1,'TEST-NET-2 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');
INSERT INTO ip_blacklist VALUES(5,'203.0.113.0/24',1,'TEST-NET-3 Documentation space - RFC 5737',NULL,'2025-01-04 11:39:08','system');

View File

@ -1,62 +0,0 @@
# 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.

View File

@ -1,33 +0,0 @@
<?php
// Logs plugin bootstrap
// (here we add any plugin autoloader, if needed)
// List here all the controllers in "/controllers/" that we need as pages
$GLOBALS['plugin_controllers']['logs'] = [
'logs'
];
// 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;
});
// 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>';
});

View File

@ -1,15 +0,0 @@
<?php
function getLogLevelClass($level) {
switch (strtolower($level)) {
case 'emergency': return 'text-danger fw-bold';
case 'alert': return 'text-danger';
case 'critical': return 'text-warning fw-bold';
case 'error': return 'text-warning';
case 'warning': return 'text-warning';
case 'notice': return 'text-primary';
case 'info': return 'text-info';
case 'debug': return 'text-muted';
default: return 'text-body'; // fallback normal text
}
}

View File

@ -1,14 +0,0 @@
-- --------------------------------------------------------
-- Logger Plugin: Create `log` table if it does not exist
-- --------------------------------------------------------
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,
`level` set('emergency','alert','critical','error','warning','notice','info','debug') NOT NULL DEFAULT 'info',
`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;

View File

@ -1,119 +0,0 @@
<?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();
}
/**
* 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 log l
LEFT JOIN user 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);
}
/**
* PSR-3 style log method - inserts a log event into the database.
*
* @param string $level The log level (emergency, alert, critical, error, warning, notice, info, debug).
* @param string $message The log message to insert.
* @param string $scope The scope of the log event (e.g., 'user', 'system'). Default is 'system'.
*/
public function log(string $level, string $message, array $context = []): void {
$userId = $context['user_id'] ?? null;
$scope = $context['scope'] ?? 'system';
try {
$sql = 'INSERT INTO log
(user_id, level, scope, message)
VALUES
(:user_id, :level, :scope, :message)';
$query = $this->db->prepare($sql);
$query->execute([
':user_id' => $userId,
':level' => $level,
':scope' => $scope,
':message' => $message,
]);
} catch (Exception $e) {
// swallowing exceptions or here we could log to error log for testing
}
}
}

View File

@ -1,34 +0,0 @@
<?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];
}
}

View File

@ -1,6 +0,0 @@
{
"name": "Logger Plugin",
"version": "1.0.1",
"enabled": true,
"description": "Initializes logging system via LoggerFactory"
}

View File

@ -1,111 +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 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" style="width: 100%;">
<thead class="table-light">
<tr>
<?php if ($scope === 'system') { ?>
<th>Username&nbsp;(id)</th>
<?php } ?>
<th style="white-space: nowrap;">Time</th>
<th style="white-space: nowrap;">Log level</th>
<th>Log message</th>
</tr>
</thead>
<tbody>
<?php foreach ($logs['records'] as $row) { ?>
<tr>
<?php if ($scope === 'system') { ?>
<td style="white-space: nowrap;"><?= $row['userID'] ? '<strong>' . htmlspecialchars($row['username'] . " ({$row['userID']})") . '</strong>' : '<span class="text-muted font-weight-normal small">SYSTEM</span>' ?></td>
<?php } ?>
<td style="white-space: nowrap;"><span class="text-muted"><?= date('d M Y H:i', strtotime($row['time'])) ?></span></td>
<td style="white-space: nowrap;"><span class="<?= getLogLevelClass($row['log level']) ?>"><?= htmlspecialchars($row['log level']) ?></span></td>
<td style="width: 100%; word-break: break-word;"><?= 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 -->

View File

@ -1,24 +0,0 @@
<?php
// Register plugin bootstrap
// (here we add any plugin autoloader, if needed)
// List here all the controllers in "/controllers/" that we need as pages
$GLOBALS['plugin_controllers']['register'] = [
'register'
];
// Add to publicly accessible pages
register_hook('filter_public_pages', function($pages) {
$pages[] = 'register';
return $pages;
});
// Configuration for main menu injection
define('REGISTRATIONS_MAIN_MENU_SECTION', 'main');
define('REGISTRATIONS_MAIN_MENU_POSITION', 30);
register_hook('main_public_menu', function($ctx) {
$section = defined('REGISTRATIONS_MAIN_MENU_SECTION') ? REGISTRATIONS_MAIN_MENU_SECTION : 'main';
$position = defined('REGISTRATIONS_MAIN_MENU_POSITION') ? REGISTRATIONS_MAIN_MENU_POSITION : 100;
echo '<li><a href="?page=register">register</a></li>';
});

View File

@ -1,92 +0,0 @@
<?php
/**
* class Register
*
* Handles user registration.
*/
class Register {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
private $rateLimiter;
private $twoFactorAuth;
/**
* Register constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
if ($database instanceof PDO) {
$this->db = $database;
} else {
$this->db = $database->getConnection();
}
require_once dirname(__FILE__, 4) . '/app/classes/ratelimiter.php';
require_once dirname(__FILE__, 4) . '/app/classes/twoFactorAuth.php';
$this->rateLimiter = new RateLimiter($database);
$this->twoFactorAuth = new TwoFactorAuthentication($database);
}
/**
* Registers a new user.
*
* @param string $username The username of the new user.
* @param string $password The password for the new user.
*
* @return bool|string True if registration is successful, error message otherwise.
*/
public function register($username, $password) {
try {
// we have two inserts, start a transaction
$this->db->beginTransaction();
// hash the password, don't store it plain
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// insert into user table
$sql = 'INSERT
INTO user (username, password)
VALUES (:username, :password)';
$query = $this->db->prepare($sql);
$query->bindValue(':username', $username);
$query->bindValue(':password', $hashedPassword);
// execute the first query
if (!$query->execute()) {
// rollback on error
$this->db->rollBack();
return false;
}
// insert the last user id into user_meta table
$sql2 = 'INSERT
INTO user_meta (user_id)
VALUES (:user_id)';
$query2 = $this->db->prepare($sql2);
$query2->bindValue(':user_id', $this->db->lastInsertId());
// execute the second query
if (!$query2->execute()) {
// rollback on error
$this->db->rollBack();
return false;
}
// if all is OK, commit the transaction
$this->db->commit();
return true;
} catch (Exception $e) {
// rollback on any error
$this->db->rollBack();
return $e->getMessage();
}
}
}

View File

@ -1,6 +0,0 @@
{
"name": "Registration Plugin",
"version": "1.0.1",
"enabled": true,
"description": "Provides registration functionality as a plugin."
}

View File

@ -11,126 +11,18 @@
* Version: 0.4 * Version: 0.4
*/ */
// error reporting, comment out in production
//ini_set('display_errors', 1);
//ini_set('display_startup_errors', 1);
//error_reporting(E_ALL);
// Prepare config loader
require_once __DIR__ . '/../app/core/ConfigLoader.php';
use App\Core\ConfigLoader;
// Load configuration
$config = ConfigLoader::loadConfig([
__DIR__ . '/../app/config/jilo-web.conf.php',
__DIR__ . '/../jilo-web.conf.php',
'/srv/jilo-web/jilo-web.conf.php',
'/opt/jilo-web/jilo-web.conf.php',
]);
// Make config available globally
$GLOBALS['config'] = $config;
// Expose config file path for pages
$config_file = ConfigLoader::getConfigPath();
$localConfigPath = str_replace(__DIR__ . '/..', '', $config_file);
// Set app root with default
$app_root = $config['folder'] ?? '/';
// Preparing plugins and hooks
// Initialize HookDispatcher and plugin system
require_once __DIR__ . '/../app/core/HookDispatcher.php';
require_once __DIR__ . '/../app/core/PluginManager.php';
use App\Core\HookDispatcher;
use App\Core\PluginManager;
// Global allowed URLs registration
register_hook('filter_allowed_urls', function($urls) {
if (isset($GLOBALS['plugin_controllers']) && is_array($GLOBALS['plugin_controllers'])) {
foreach ($GLOBALS['plugin_controllers'] as $controllers) {
foreach ($controllers as $ctrl) {
$urls[] = $ctrl;
}
}
}
return $urls;
});
// Hook registration and dispatch helpers
function register_hook(string $hook, callable $callback): void {
HookDispatcher::register($hook, $callback);
}
function do_hook(string $hook, array $context = []): void {
HookDispatcher::dispatch($hook, $context);
}
function filter_public_pages(array $pages): array {
return HookDispatcher::applyFilters('filter_public_pages', $pages);
}
function filter_allowed_urls(array $urls): array {
return HookDispatcher::applyFilters('filter_allowed_urls', $urls);
}
// Load enabled plugins
$plugins_dir = dirname(__DIR__) . '/plugins/';
$enabled_plugins = PluginManager::load($plugins_dir);
$GLOBALS['enabled_plugins'] = $enabled_plugins;
// Define CSRF token include path globally
if (!defined('CSRF_TOKEN_INCLUDE')) {
define('CSRF_TOKEN_INCLUDE', dirname(__DIR__) . '/app/includes/csrf_token.php');
}
// Global cnstants
require_once '../app/includes/constants.php';
// we start output buffering and // we start output buffering and
// flush it later only when there is no redirect // flush it later only when there is no redirect
ob_start(); ob_start();
// Start session before any session-dependent code // Apply security headers
require_once '../app/classes/session.php'; require_once '../app/includes/security_headers_middleware.php';
// Initialize themes system after session is started // sanitize all input vars that may end up in URLs or forms
require_once __DIR__ . '/../app/helpers/theme.php'; require '../app/includes/sanitize.php';
use app\Helpers\Theme;
Session::startSession(); session_name('jilo');
session_start();
// Define page variable early via sanitize
require_once __DIR__ . '/../app/includes/sanitize.php';
// Ensure $page is defined to avoid undefined variable
if (!isset($page)) {
$page = 'dashboard';
}
// List of pages that don't require authentication
$public_pages = ['login', 'help', 'about'];
// Let plugins filter/extend public_pages
$public_pages = filter_public_pages($public_pages);
// Middleware pipeline for security, sanitization & CSRF
require_once __DIR__ . '/../app/core/MiddlewarePipeline.php';
$pipeline = new \App\Core\MiddlewarePipeline();
$pipeline->add(function() {
// Apply security headers
require_once __DIR__ . '/../app/includes/security_headers_middleware.php';
return true;
});
// For public pages, we don't need to validate the session
// The Router will handle authentication for protected pages
$validSession = false;
$userId = null;
// Only check session for non-public pages
if (!in_array($page, $public_pages)) {
$validSession = Session::isValidSession(true);
if ($validSession) {
$userId = Session::getUserId();
}
}
// Initialize feedback message system // Initialize feedback message system
require_once '../app/classes/feedback.php'; require_once '../app/classes/feedback.php';
@ -138,81 +30,120 @@ $system_messages = [];
require '../app/includes/errors.php'; require '../app/includes/errors.php';
// error reporting, comment out in production
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// list of available pages // list of available pages
// edit accordingly, add 'pages/PAGE.php' // edit accordingly, add 'pages/PAGE.php'
$allowed_urls = [ $allowed_urls = [
'dashboard', 'dashboard',
'conferences','participants','components',
'graphs','latest','livejs','agents', 'conferences',
'profile','credentials','config','security', 'participants',
'settings','theme','theme-asset', 'components',
'graphs',
'latest',
'livejs',
'agents',
'config',
'profile',
'credentials',
'settings',
'security',
'status', 'status',
'help','about', 'logs',
'login','logout', 'help',
'login',
'logout',
'register',
]; ];
// Let plugins filter/extend allowed_urls // cnfig file
$allowed_urls = filter_allowed_urls($allowed_urls); // possible locations, in order of preference
$config_file_locations = [
// Dispatch routing and auth __DIR__ . '/../app/config/jilo-web.conf.php',
require_once __DIR__ . '/../app/core/Router.php'; __DIR__ . '/../jilo-web.conf.php',
use App\Core\Router; '/srv/jilo-web/jilo-web.conf.php',
$currentUser = Router::checkAuth($config, $app_root, $public_pages, $page); '/opt/jilo-web/jilo-web.conf.php'
];
// Connect to DB via DatabaseConnector $config_file = null;
require_once __DIR__ . '/../app/core/DatabaseConnector.php'; // try to find the config file
use App\Core\DatabaseConnector; foreach ($config_file_locations as $location) {
$db = DatabaseConnector::connect($config); if (file_exists($location)) {
$config_file = $location;
// Logging: default to NullLogger, plugin can override break;
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' => $db]);
// Override defaults if plugin provided real logger
if (isset($GLOBALS['logObject'])) {
$logObject = $GLOBALS['logObject'];
} }
if (isset($GLOBALS['user_IP'])) { // if found, use it
$user_IP = $GLOBALS['user_IP']; if ($config_file) {
$localConfigPath = str_replace(__DIR__ . '/..', '', $config_file);
$config = require $config_file;
} else {
die('Config file not found');
} }
// CSRF middleware and run pipeline $app_root = $config['folder'];
$pipeline->add(function() {
// Initialize security middleware // check if logged in
require_once __DIR__ . '/../app/includes/csrf_middleware.php'; unset($currentUser);
require_once __DIR__ . '/../app/helpers/security.php'; if (isset($_COOKIE['username'])) {
$security = SecurityHelper::getInstance(); if ( !isset($_SESSION['username']) ) {
// Verify CSRF token for POST requests $_SESSION['username'] = $_COOKIE['username'];
return applyCsrfMiddleware(); }
}); $currentUser = htmlspecialchars($_SESSION['username']);
$pipeline->add(function() {
// Init rate limiter
global $db, $page, $userId;
require_once __DIR__ . '/../app/includes/rate_limit_middleware.php';
return checkRateLimit($db, $page, $userId);
});
$pipeline->add(function() {
// Init user functions
global $db, $userObject;
require_once __DIR__ . '/../app/classes/user.php';
include __DIR__ . '/../app/helpers/profile.php';
$userObject = new User($db);
return true;
});
if (!$pipeline->run()) {
exit;
} }
// redirect to login
if ( !isset($_COOKIE['username']) && ($page !== 'login' && $page !== 'register') ) {
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
exit();
}
// connect to db of Jilo Web
require '../app/classes/database.php';
require '../app/includes/database.php';
try {
$response = connectDB($config);
if (!$response['db']) {
throw new Exception('Could not connect to database: ' . $response['error']);
}
$dbWeb = $response['db'];
} catch (Exception $e) {
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to the database.', $e->getMessage()));
include '../app/templates/page-header.php';
include '../app/helpers/feedback.php';
include '../app/templates/page-footer.php';
exit();
}
// start logging
require '../app/classes/log.php';
include '../app/helpers/logs.php';
$logObject = new Log($dbWeb);
$user_IP = getUserIP();
// Initialize security middleware
require_once '../app/includes/csrf_middleware.php';
require_once '../app/helpers/security.php';
$security = SecurityHelper::getInstance();
// Verify CSRF token for POST requests
applyCsrfMiddleware();
// init rate limiter
require '../app/classes/ratelimiter.php';
// get platforms details // get platforms details
require '../app/classes/platform.php'; require '../app/classes/platform.php';
$platformObject = new Platform($db); $platformObject = new Platform($dbWeb);
$platformsAll = $platformObject->getPlatformDetails(); $platformsAll = $platformObject->getPlatformDetails();
// by default we connect ot the first configured platform // by default we connect ot the first configured platform
@ -222,50 +153,54 @@ if ($platform_id == '') {
$platformDetails = $platformObject->getPlatformDetails($platform_id); $platformDetails = $platformObject->getPlatformDetails($platform_id);
// init user functions
require '../app/classes/user.php';
include '../app/helpers/profile.php';
$userObject = new User($dbWeb);
// logout is a special case, as we can't use session vars for notices // logout is a special case, as we can't use session vars for notices
if ($page == 'logout') { if ($page == 'logout') {
// Save config before destroying session // get user info before destroying session
$savedConfig = $config; $user_id = $userObject->getUserId($currentUser)[0]['id'];
// clean up session // clean up session
Session::destroySession(); session_unset();
session_destroy();
// start new session for the login page // start new session for the login page
Session::startSession(); session_start();
// Restore config to global scope
$config = $savedConfig;
$GLOBALS['config'] = $config;
setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true); setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true);
// Log successful logout // Log successful logout
$logObject->log('info', "Logout: User \"$currentUser\" logged out. IP: $user_IP", ['user_id' => $userId, 'scope' => 'user']); $logObject->insertLog($user_id, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user');
// Set success message // Set success message
Feedback::flash('LOGIN', 'LOGOUT_SUCCESS'); Feedback::flash('LOGIN', 'LOGOUT_SUCCESS');
// Use theme helper to include templates include '../app/templates/page-header.php';
\App\Helpers\Theme::include('page-header'); include '../app/templates/page-menu.php';
\App\Helpers\Theme::include('page-menu');
include '../app/pages/login.php'; include '../app/pages/login.php';
\App\Helpers\Theme::include('page-footer'); include '../app/templates/page-footer.php';
} else { } else {
// if user is logged in, we need user details and rights // if user is logged in, we need user details and rights
if ($validSession) { if (isset($currentUser)) {
// If by error a logged in user requests the login page // If by error a logged in user requests the login page
if ($page === 'login') { if ($page === 'login') {
header('Location: ' . htmlspecialchars($app_root)); header('Location: ' . htmlspecialchars($app_root));
exit(); exit();
} }
$userDetails = $userObject->getUserDetails($userId); $user_id = $userObject->getUserId($currentUser)[0]['id'];
$userRights = $userObject->getUserRights($userId); $userDetails = $userObject->getUserDetails($user_id);
$userRights = $userObject->getUserRights($user_id);
$userTimezone = (!empty($userDetails[0]['timezone'])) ? $userDetails[0]['timezone'] : 'UTC'; // Default to UTC if no timezone is set (or is missing) $userTimezone = (!empty($userDetails[0]['timezone'])) ? $userDetails[0]['timezone'] : 'UTC'; // Default to UTC if no timezone is set (or is missing)
// check if the Jilo Server is running // check if the Jilo Server is running
require '../app/classes/server.php'; require '../app/classes/server.php';
$serverObject = new Server($db); $serverObject = new Server($dbWeb);
$server_host = '127.0.0.1'; $server_host = '127.0.0.1';
$server_port = '8080'; $server_port = '8080';
@ -276,70 +211,28 @@ if ($page == 'logout') {
} }
} }
// --- Plugin loading logic for all enabled plugins --- // List of pages that don't require authentication
// Ensure all enabled plugin bootstraps are loaded before mapping controllers $public_pages = ['login', 'register'];
foreach ($GLOBALS['enabled_plugins'] as $plugin_name => $plugin_info) {
$bootstrap_path = $plugin_info['path'] . '/bootstrap.php'; // Check if the requested page requires authentication
if (file_exists($bootstrap_path)) { if (!in_array($page, $public_pages)) {
require_once $bootstrap_path; require_once '../app/includes/session_middleware.php';
}
}
// Plugin controller mapping logic (we add each controller listed in bootstrap as a page)
$mapped_plugin_controllers = [];
foreach ($GLOBALS['enabled_plugins'] as $plugin_name => $plugin_info) {
if (isset($GLOBALS['plugin_controllers'][$plugin_name])) {
foreach ($GLOBALS['plugin_controllers'][$plugin_name] as $plugin_page) {
$controller_path = $plugin_info['path'] . '/controllers/' . $plugin_page . '.php';
if (file_exists($controller_path)) {
$mapped_plugin_controllers[$plugin_page] = $controller_path;
}
}
}
} }
// page building // page building
if (in_array($page, $allowed_urls)) { include '../app/templates/page-header.php';
// The page is in allowed URLs include '../app/templates/page-menu.php';
if (isset($mapped_plugin_controllers[$page]) && file_exists($mapped_plugin_controllers[$page])) { if (isset($currentUser)) {
// The page is from a plugin controller include '../app/templates/page-sidebar.php';
if (defined('PLUGIN_PAGE_DIRECT_OUTPUT') && PLUGIN_PAGE_DIRECT_OUTPUT === true) {
// Barebone page controller, we don't output anything extra
include $mapped_plugin_controllers[$page];
ob_end_flush();
exit;
} else {
\App\Helpers\Theme::include('page-header');
\App\Helpers\Theme::include('page-menu');
if ($validSession) {
\App\Helpers\Theme::include('page-sidebar');
}
include $mapped_plugin_controllers[$page];
\App\Helpers\Theme::include('page-footer');
}
} else {
// The page is from a core controller
\App\Helpers\Theme::include('page-header');
\App\Helpers\Theme::include('page-menu');
if ($validSession) {
\App\Helpers\Theme::include('page-sidebar');
}
if (file_exists("../app/pages/{$page}.php")) {
include "../app/pages/{$page}.php";
} else {
include '../app/templates/error-notfound.php';
}
\App\Helpers\Theme::include('page-footer');
}
} else {
// The page is not in allowed URLs
\App\Helpers\Theme::include('page-header');
\App\Helpers\Theme::include('page-menu');
if ($validSession) {
\App\Helpers\Theme::include('page-sidebar');
}
include '../app/templates/error-notfound.php';
\App\Helpers\Theme::include('page-footer');
} }
if (in_array($page, $allowed_urls)) {
// all normal pages
include "../app/pages/{$page}.php";
} else {
// the page is not in allowed urls, loading "not found" page
include '../app/templates/error-notfound.php';
}
include '../app/templates/page-footer.php';
} }
// flush the output buffer and show the page // flush the output buffer and show the page

View File

@ -1,7 +0,0 @@
<?php
namespace Tests\Feature\Middleware\Mock;
class Feedback {
public static function flash($type, $message) {}
}

View File

@ -1,19 +0,0 @@
<?php
namespace Tests\Feature\Middleware\Mock;
class Session {
public static function startSession() {}
public static function isValidSession() {
return isset($_SESSION["user_id"]) &&
isset($_SESSION["username"]) &&
(!isset($_SESSION["LAST_ACTIVITY"]) ||
$_SESSION["LAST_ACTIVITY"] > time() - 7200 ||
isset($_SESSION["REMEMBER_ME"]));
}
public static function cleanup($config) {
$_SESSION = [];
}
}

View File

@ -16,235 +16,137 @@ class RateLimitMiddlewareTest extends TestCase
{ {
parent::setUp(); parent::setUp();
// Set global IP for rate limiting
global $user_IP;
$user_IP = '8.8.8.8';
// Prepare DB for Github CI
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
// Set up test database // Set up test database
$this->db = new Database([ $this->db = new Database([
'type' => 'mariadb', 'type' => 'sqlite',
'host' => $host, 'dbFile' => ':memory:'
'port' => '3306',
'dbname' => 'jilo_test',
'user' => 'test_jilo',
'password' => $password
]); ]);
// Create rate limiter instance // Create rate limiter table
$this->db->getConnection()->exec("CREATE TABLE pages_rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
endpoint TEXT NOT NULL,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP
)");
// Create ip_whitelist table
$this->db->getConnection()->exec("CREATE TABLE ip_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description TEXT,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
)");
// Create ip_blacklist table
$this->db->getConnection()->exec("CREATE TABLE ip_blacklist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT,
expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
)");
$this->rateLimiter = new RateLimiter($this->db); $this->rateLimiter = new RateLimiter($this->db);
// Drop tables if they exist // Mock $_SERVER variables
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth"); $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_page"); $_SERVER['REQUEST_URI'] = '/login';
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_ip_blacklist"); $_SERVER['REQUEST_METHOD'] = 'POST';
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_ip_whitelist");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS log");
// Create required tables with correct names from RateLimiter class // Define testing constant
$this->db->getConnection()->exec(" if (!defined('TESTING')) {
CREATE TABLE IF NOT EXISTS security_rate_auth ( define('TESTING', true);
id INT PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
username VARCHAR(255) NOT NULL,
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip_username (ip_address, username)
)
");
$this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS security_rate_page (
id INT PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
endpoint VARCHAR(255) NOT NULL,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip_endpoint (ip_address, endpoint),
INDEX idx_request_time (request_time)
)
");
$this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS security_ip_blacklist (
id INT PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
is_network BOOLEAN DEFAULT FALSE,
reason VARCHAR(255),
expiry_time TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255),
UNIQUE KEY unique_ip (ip_address)
)
");
$this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS security_ip_whitelist (
id INT PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
is_network BOOLEAN DEFAULT FALSE,
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255),
UNIQUE KEY unique_ip (ip_address)
)
");
// Create log table
$this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS log (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
scope VARCHAR(50) NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
// Mock $_SERVER['REMOTE_ADDR'] with a non-whitelisted IP
$_SERVER['REMOTE_ADDR'] = '8.8.8.8';
// Define PHPUNIT_RUNNING constant
if (!defined('PHPUNIT_RUNNING')) {
define('PHPUNIT_RUNNING', true);
} }
} }
protected function tearDown(): void protected function tearDown(): void
{ {
// Clean up all rate limit records // Clean up rate limit records
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page"); $this->db->getConnection()->exec('DELETE FROM pages_rate_limits');
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_blacklist");
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_whitelist");
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_auth");
$this->db->getConnection()->exec("TRUNCATE TABLE log");
parent::tearDown(); parent::tearDown();
} }
public function testRateLimitMiddleware() public function testRateLimitMiddleware()
{ {
// Clean any existing rate limit records // Test multiple requests
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page"); for ($i = 1; $i <= 5; $i++) {
// Make 60 requests to reach the limit
for ($i = 0; $i < 60; $i++) {
$result = checkRateLimit($this->db, '/login'); $result = checkRateLimit($this->db, '/login');
$this->assertTrue($result, "Request $i should be allowed");
// Verify request was recorded if ($i <= 5) {
$stmt = $this->db->getConnection()->prepare(" // First 5 requests should pass
SELECT COUNT(*) as count $this->assertTrue($result);
FROM security_rate_page } else {
WHERE ip_address = ? // 6th and subsequent requests should be blocked
AND endpoint = ? $this->assertFalse($result);
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) }
");
$stmt->execute(['8.8.8.8', '/login']);
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
$this->assertEquals($i + 1, $count, "Expected " . ($i + 1) . " requests to be recorded, got {$count}");
} }
// The 61st request should be blocked
$result = checkRateLimit($this->db, '/login');
$this->assertFalse($result, "Request should be blocked after 60 requests");
} }
public function testRateLimitBypass() public function testRateLimitBypass()
{ {
// Clean any existing rate limit records and lists // Test AJAX request bypass
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page"); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest';
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_whitelist"); $result = checkRateLimit($this->db, '/login');
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_blacklist"); $this->assertTrue($result);
// Add IP to whitelist and verify it was added
$stmt = $this->db->getConnection()->prepare("INSERT INTO security_ip_whitelist (ip_address, is_network, description, created_by) VALUES (?, 0, ?, 'PHPUnit')");
$stmt->execute(['8.8.8.8', 'Test whitelist']);
// Verify IP is in whitelist
$stmt = $this->db->getConnection()->prepare("SELECT COUNT(*) as count FROM security_ip_whitelist WHERE ip_address = ?");
$stmt->execute(['8.8.8.8']);
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
$this->assertEquals(1, $count, "IP should be in whitelist");
// Should be able to make more requests than limit
for ($i = 0; $i < 100; $i++) {
$result = checkRateLimit($this->db, '/login');
$this->assertTrue($result, "Request $i should be allowed for whitelisted IP");
}
} }
public function testRateLimitReset() public function testRateLimitReset()
{ {
// Clean any existing rate limit records // Use up the rate limit
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page"); for ($i = 0; $i < 5; $i++) {
checkRateLimit($this->db, '/login');
// Make some requests
for ($i = 0; $i < 15; $i++) {
$result = checkRateLimit($this->db, '/login');
} }
// Manually expire old requests // Wait for rate limit to reset (use a short window for testing)
$this->db->getConnection()->exec(" sleep(2);
DELETE FROM security_rate_page
WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)
");
// Should be able to make requests again // Should be able to make request again
$result = checkRateLimit($this->db, '/login'); $result = checkRateLimit($this->db, '/login');
$this->assertTrue($result); $this->assertTrue($result);
} }
public function testDifferentEndpoints() public function testDifferentEndpoints()
{ {
// Clean any existing rate limit records // Use up rate limit for login
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
// Make requests to login endpoint (default limit: 60)
for ($i = 0; $i < 30; $i++) {
$result = checkRateLimit($this->db, '/login');
$this->assertTrue($result, "Request $i to /login should be allowed");
}
// Clean up between endpoint tests
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
// Make requests to register endpoint (limit: 5)
for ($i = 0; $i < 5; $i++) { for ($i = 0; $i < 5; $i++) {
$result = checkRateLimit($this->db, '/register'); checkRateLimit($this->db, '/login');
$this->assertTrue($result, "Request $i to /register should be allowed");
} }
// The 6th request to register should be blocked // Should still be able to access different endpoint
$result = checkRateLimit($this->db, '/register'); $result = checkRateLimit($this->db, '/dashboard');
$this->assertFalse($result, "Request should be blocked after 5 requests to /register"); $this->assertTrue($result);
} }
public function testDifferentIpAddresses() public function testDifferentIpAddresses()
{ {
// Make requests from first IP // Use up rate limit for first IP
for ($i = 0; $i < 30; $i++) { $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$result = checkRateLimit($this->db, '/login'); for ($i = 0; $i < 5; $i++) {
$this->assertTrue($result); checkRateLimit($this->db, '/login');
} }
// Change IP // Different IP should not be affected
$_SERVER['REMOTE_ADDR'] = '8.8.4.4'; $_SERVER['REMOTE_ADDR'] = '127.0.0.2';
$result = checkRateLimit($this->db, '/login');
// Should be able to make requests from different IP $this->assertTrue($result);
for ($i = 0; $i < 30; $i++) {
$result = checkRateLimit($this->db, '/login');
$this->assertTrue($result);
}
} }
public function testWhitelistedIp() public function testWhitelistedIp()
{ {
// Add IP to whitelist // Add IP to whitelist
$this->rateLimiter->addToWhitelist('8.8.8.8', false, 'Test whitelist', 'PHPUnit'); $this->db->execute(
'INSERT INTO ip_whitelist (ip_address, description, created_by) VALUES (?, ?, ?)',
['127.0.0.1', 'Test whitelist', 'PHPUnit']
);
// Should be able to make more requests than limit // Should be able to make more requests than limit
for ($i = 0; $i < 50; $i++) { for ($i = 0; $i < 10; $i++) {
$result = checkRateLimit($this->db, '/login'); $result = checkRateLimit($this->db, '/login');
$this->assertTrue($result); $this->assertTrue($result);
} }
@ -252,39 +154,30 @@ class RateLimitMiddlewareTest extends TestCase
public function testBlacklistedIp() public function testBlacklistedIp()
{ {
// Add IP to blacklist and verify it was added // Add IP to blacklist
$this->db->getConnection()->exec("INSERT INTO security_ip_blacklist (ip_address, is_network, reason, created_by) VALUES ('8.8.8.8', 0, 'Test blacklist', 'system')"); $this->db->execute(
'INSERT INTO ip_blacklist (ip_address, reason, created_by) VALUES (?, ?, ?)',
['127.0.0.1', 'Test blacklist', 'PHPUnit']
);
// Request should be blocked // Should be blocked immediately
$result = checkRateLimit($this->db, '/login'); $result = checkRateLimit($this->db, '/login');
$this->assertFalse($result, "Blacklisted IP should be blocked"); $this->assertFalse($result);
} }
public function testRateLimitPersistence() public function testRateLimitPersistence()
{ {
// Clean any existing rate limit records // Use up some of the rate limit
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page"); for ($i = 0; $i < 2; $i++) {
checkRateLimit($this->db, '/login');
// Make 60 requests to reach the limit
for ($i = 0; $i < 60; $i++) {
$result = checkRateLimit($this->db, '/login');
$this->assertTrue($result, "Request $i should be allowed");
// Verify request was recorded
$stmt = $this->db->getConnection()->prepare("
SELECT COUNT(*) as count
FROM security_rate_page
WHERE ip_address = ?
AND endpoint = ?
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)
");
$stmt->execute(['8.8.8.8', '/login']);
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
$this->assertEquals($i + 1, $count, "Expected " . ($i + 1) . " requests to be recorded, got {$count}");
} }
// The 61st request should be blocked // Destroy and restart session
//session_destroy();
//session_start();
// Should still count previous requests
$result = checkRateLimit($this->db, '/login'); $result = checkRateLimit($this->db, '/login');
$this->assertFalse($result, "Request should be blocked after 60 requests"); $this->assertTrue($result);
} }
} }

View File

@ -1,11 +1,8 @@
<?php <?php
use PHPUnit\Framework\TestCase; require_once dirname(__DIR__, 3) . '/app/includes/session_middleware.php';
use Tests\Feature\Middleware\Mock\Session;
use Tests\Feature\Middleware\Mock\Feedback;
require_once __DIR__ . '/MockSession.php'; use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/MockFeedback.php';
class SessionMiddlewareTest extends TestCase class SessionMiddlewareTest extends TestCase
{ {
@ -41,24 +38,11 @@ class SessionMiddlewareTest extends TestCase
protected function tearDown(): void protected function tearDown(): void
{ {
parent::tearDown(); parent::tearDown();
$_SESSION = [];
} }
protected function applyMiddleware() public function testSessionStart()
{ {
// Check session validity $result = applySessionMiddleware($this->config, $this->app_root);
if (!Session::isValidSession()) {
// Session invalid, clean up
Session::cleanup($this->config);
Feedback::flash("LOGIN", "SESSION_TIMEOUT");
return false;
}
return true;
}
public function testValidSession()
{
$result = $this->applyMiddleware();
$this->assertTrue($result); $this->assertTrue($result);
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION); $this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
@ -70,10 +54,24 @@ class SessionMiddlewareTest extends TestCase
public function testSessionTimeout() public function testSessionTimeout()
{ {
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago $_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago
$result = $this->applyMiddleware();
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result); $this->assertFalse($result);
$this->assertEmpty($_SESSION); $this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout');
}
public function testSessionRegeneration()
{
$now = time();
$_SESSION['CREATED'] = $now - 1900; // 31+ minutes ago
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertTrue($result);
$this->assertEquals(1, $_SESSION['user_id']);
$this->assertGreaterThanOrEqual($now - 1900, $_SESSION['CREATED']);
$this->assertLessThanOrEqual($now + 10, $_SESSION['CREATED']);
} }
public function testRememberMe() public function testRememberMe()
@ -81,7 +79,7 @@ class SessionMiddlewareTest extends TestCase
$_SESSION['REMEMBER_ME'] = true; $_SESSION['REMEMBER_ME'] = true;
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // More than 2 hours ago $_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // More than 2 hours ago
$result = $this->applyMiddleware(); $result = applySessionMiddleware($this->config, $this->app_root);
$this->assertTrue($result); $this->assertTrue($result);
$this->assertArrayHasKey('user_id', $_SESSION); $this->assertArrayHasKey('user_id', $_SESSION);
@ -90,19 +88,19 @@ class SessionMiddlewareTest extends TestCase
public function testNoUserSession() public function testNoUserSession()
{ {
unset($_SESSION['user_id']); unset($_SESSION['user_id']);
$result = $this->applyMiddleware(); $result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result); $this->assertFalse($result);
$this->assertEmpty($_SESSION); $this->assertArrayNotHasKey('user_id', $_SESSION);
} }
public function testInvalidSession() public function testSessionHeaders()
{ {
$_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago $_SESSION['LAST_ACTIVITY'] = time() - (self::SESSION_TIMEOUT + 60); // 2 hours + 1 minute ago
unset($_SESSION['REMEMBER_ME']);
$result = $this->applyMiddleware(); $result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result); $this->assertFalse($result);
$this->assertEmpty($_SESSION); $this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout');
} }
} }

View File

@ -8,7 +8,7 @@ require_once dirname(__DIR__, 3) . '/app/helpers/security.php';
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class TestLogger { class TestLogger {
public static function insertLog($userId, $message, $scope = 'user') { public static function insertLog($user_id, $message, $scope = 'user') {
return true; return true;
} }
} }

View File

@ -24,9 +24,9 @@ class AgentTest extends TestCase
'dbFile' => ':memory:' 'dbFile' => ':memory:'
]); ]);
// Create jilo_agent table // Create jilo_agents table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE jilo_agent ( CREATE TABLE jilo_agents (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL, host_id INTEGER NOT NULL,
agent_type_id INTEGER NOT NULL, agent_type_id INTEGER NOT NULL,
@ -38,18 +38,18 @@ class AgentTest extends TestCase
) )
"); ");
// Create jilo_agent_type table // Create jilo_agent_types table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE jilo_agent_type ( CREATE TABLE jilo_agent_types (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL, description TEXT NOT NULL,
endpoint TEXT NOT NULL endpoint TEXT NOT NULL
) )
"); ");
// Create host table // Create hosts table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE host ( CREATE TABLE hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
platform_id INTEGER NOT NULL, platform_id INTEGER NOT NULL,
name TEXT NOT NULL name TEXT NOT NULL
@ -58,12 +58,12 @@ class AgentTest extends TestCase
// Insert test host // Insert test host
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
INSERT INTO host (id, platform_id, name) VALUES (1, 1, 'Test Host') INSERT INTO hosts (id, platform_id, name) VALUES (1, 1, 'Test Host')
"); ");
// Insert test agent type // Insert test agent type
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
INSERT INTO jilo_agent_type (id, description, endpoint) INSERT INTO jilo_agent_types (id, description, endpoint)
VALUES (1, 'Test Agent Type', '/api/test') VALUES (1, 'Test Agent Type', '/api/test')
"); ");
@ -85,7 +85,7 @@ class AgentTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify agent was created // Verify agent was created
$stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agent WHERE host_id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agents WHERE host_id = ?');
$stmt->execute([$hostId]); $stmt->execute([$hostId]);
$agent = $stmt->fetch(PDO::FETCH_ASSOC); $agent = $stmt->fetch(PDO::FETCH_ASSOC);
@ -131,7 +131,7 @@ class AgentTest extends TestCase
$this->agent->addAgent($hostId, $data); $this->agent->addAgent($hostId, $data);
// Get agent ID // Get agent ID
$stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agent WHERE host_id = ? LIMIT 1'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agents WHERE host_id = ? LIMIT 1');
$stmt->execute([$hostId]); $stmt->execute([$hostId]);
$agentId = $stmt->fetch(PDO::FETCH_COLUMN); $agentId = $stmt->fetch(PDO::FETCH_COLUMN);
@ -148,7 +148,7 @@ class AgentTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify update // Verify update
$stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agent WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agents WHERE id = ?');
$stmt->execute([$agentId]); $stmt->execute([$agentId]);
$agent = $stmt->fetch(PDO::FETCH_ASSOC); $agent = $stmt->fetch(PDO::FETCH_ASSOC);
@ -171,7 +171,7 @@ class AgentTest extends TestCase
$this->agent->addAgent($hostId, $data); $this->agent->addAgent($hostId, $data);
// Get agent ID // Get agent ID
$stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agent WHERE host_id = ? LIMIT 1'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agents WHERE host_id = ? LIMIT 1');
$stmt->execute([$hostId]); $stmt->execute([$hostId]);
$agentId = $stmt->fetch(PDO::FETCH_COLUMN); $agentId = $stmt->fetch(PDO::FETCH_COLUMN);
@ -180,7 +180,7 @@ class AgentTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify deletion // Verify deletion
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agent WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agents WHERE id = ?');
$stmt->execute([$agentId]); $stmt->execute([$agentId]);
$count = $stmt->fetch(PDO::FETCH_COLUMN); $count = $stmt->fetch(PDO::FETCH_COLUMN);
@ -201,7 +201,7 @@ class AgentTest extends TestCase
$this->agent->addAgent($hostId, $data); $this->agent->addAgent($hostId, $data);
// Get agent ID // Get agent ID
$stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agent WHERE host_id = ? LIMIT 1'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agents WHERE host_id = ? LIMIT 1');
$stmt->execute([$hostId]); $stmt->execute([$hostId]);
$agentId = $stmt->fetch(PDO::FETCH_COLUMN); $agentId = $stmt->fetch(PDO::FETCH_COLUMN);

View File

@ -23,9 +23,9 @@ class HostTest extends TestCase
'dbFile' => ':memory:' 'dbFile' => ':memory:'
]); ]);
// Create host table // Create hosts table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE host ( CREATE TABLE hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
platform_id INTEGER NOT NULL, platform_id INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
@ -33,9 +33,9 @@ class HostTest extends TestCase
) )
"); ");
// Create jilo_agent table for relationship testing // Create jilo_agents table for relationship testing
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE jilo_agent ( CREATE TABLE jilo_agents (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL, host_id INTEGER NOT NULL,
agent_type_id INTEGER NOT NULL, agent_type_id INTEGER NOT NULL,
@ -60,7 +60,7 @@ class HostTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify host was created // Verify host was created
$stmt = $this->db->getConnection()->prepare('SELECT * FROM host WHERE platform_id = ? AND name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM hosts WHERE platform_id = ? AND name = ?');
$stmt->execute([$data['platform_id'], $data['name']]); $stmt->execute([$data['platform_id'], $data['name']]);
$host = $stmt->fetch(\PDO::FETCH_ASSOC); $host = $stmt->fetch(\PDO::FETCH_ASSOC);
@ -107,7 +107,7 @@ class HostTest extends TestCase
$this->host->addHost($data); $this->host->addHost($data);
// Get host ID // Get host ID
$stmt = $this->db->getConnection()->prepare('SELECT id FROM host WHERE platform_id = ? AND name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM hosts WHERE platform_id = ? AND name = ?');
$stmt->execute([$data['platform_id'], $data['name']]); $stmt->execute([$data['platform_id'], $data['name']]);
$hostId = $stmt->fetch(\PDO::FETCH_COLUMN); $hostId = $stmt->fetch(\PDO::FETCH_COLUMN);
@ -122,7 +122,7 @@ class HostTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify update // Verify update
$stmt = $this->db->getConnection()->prepare('SELECT * FROM host WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM hosts WHERE id = ?');
$stmt->execute([$hostId]); $stmt->execute([$hostId]);
$host = $stmt->fetch(\PDO::FETCH_ASSOC); $host = $stmt->fetch(\PDO::FETCH_ASSOC);
@ -141,13 +141,13 @@ class HostTest extends TestCase
$this->host->addHost($data); $this->host->addHost($data);
// Get host ID // Get host ID
$stmt = $this->db->getConnection()->prepare('SELECT id FROM host WHERE platform_id = ? AND name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM hosts WHERE platform_id = ? AND name = ?');
$stmt->execute([$data['platform_id'], $data['name']]); $stmt->execute([$data['platform_id'], $data['name']]);
$hostId = $stmt->fetch(\PDO::FETCH_COLUMN); $hostId = $stmt->fetch(\PDO::FETCH_COLUMN);
// Add test agent to the host // Add test agent to the host
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
INSERT INTO jilo_agent (host_id, agent_type_id, url, secret_key) INSERT INTO jilo_agents (host_id, agent_type_id, url, secret_key)
VALUES ($hostId, 1, 'http://test:8080', 'secret') VALUES ($hostId, 1, 'http://test:8080', 'secret')
"); ");
@ -156,13 +156,13 @@ class HostTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify host deletion // Verify host deletion
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM host WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM hosts WHERE id = ?');
$stmt->execute([$hostId]); $stmt->execute([$hostId]);
$hostCount = $stmt->fetch(\PDO::FETCH_COLUMN); $hostCount = $stmt->fetch(\PDO::FETCH_COLUMN);
$this->assertEquals(0, $hostCount); $this->assertEquals(0, $hostCount);
// Verify agent deletion // Verify agent deletion
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agent WHERE host_id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agents WHERE host_id = ?');
$stmt->execute([$hostId]); $stmt->execute([$hostId]);
$agentCount = $stmt->fetch(\PDO::FETCH_COLUMN); $agentCount = $stmt->fetch(\PDO::FETCH_COLUMN);
$this->assertEquals(0, $agentCount); $this->assertEquals(0, $agentCount);

View File

@ -1,261 +1,138 @@
<?php <?php
require_once dirname(__DIR__, 3) . '/app/classes/database.php'; require_once dirname(__DIR__, 3) . '/app/classes/database.php';
require_once dirname(__DIR__, 3) . '/app/classes/log.php';
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/**
* TestLogger class for testing log functionality
* This is a simplified implementation that mimics the plugin's Log class
* but with a different name to avoid conflicts
*/
class TestLogger {
private $db;
public function __construct($database) {
$this->db = $database->getConnection();
}
public function insertLog($userId, $message, $scope = 'user') {
try {
$sql = 'INSERT INTO log
(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();
}
}
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 log l
LEFT JOIN user 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);
}
}
class LogTest extends TestCase class LogTest extends TestCase
{ {
private $db; private $db;
private $log; private $log;
private $testUserId;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
// Prepare DB for Github CI
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
// Set up test database // Set up test database
$this->db = new Database([ $this->db = new Database([
'type' => 'mariadb', 'type' => 'sqlite',
'host' => $host, 'dbFile' => ':memory:'
'port' => '3306',
'dbname' => 'jilo_test',
'user' => 'test_jilo',
'password' => $password
]); ]);
// Create user table // Create users table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS user ( CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL, username TEXT NOT NULL
password VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
"); ");
// Create log table with the expected schema from Log class // Create test user
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS log ( INSERT INTO users (id, username) VALUES (1, 'testuser'), (2, 'testuser2')
id INT AUTO_INCREMENT PRIMARY KEY, ");
user_id INT,
scope VARCHAR(50) NOT NULL DEFAULT 'user', // Create logs table
$this->db->getConnection()->exec("
CREATE TABLE logs (
id INTEGER PRIMARY KEY,
user_id INTEGER,
scope TEXT NOT NULL,
message TEXT NOT NULL, message TEXT NOT NULL,
time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, time DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id) FOREIGN KEY (user_id) REFERENCES users(id)
) )
"); ");
// Create test users with all required fields $this->log = new Log($this->db);
$this->db->getConnection()->exec("
INSERT INTO user (username, password, email)
VALUES
('testuser', 'password123', 'testuser@example.com'),
('testuser2', 'password123', 'testuser2@example.com')
");
// Store test user ID for later use
$stmt = $this->db->getConnection()->query("SELECT id FROM user WHERE username = 'testuser' LIMIT 1");
$user = $stmt->fetch(PDO::FETCH_ASSOC);
$this->testUserId = $user['id'];
// Create a TestLogger instance that will be used by the app's Log wrapper
$this->log = new TestLogger($this->db);
}
protected function tearDown(): void
{
// Drop tables in correct order (respect foreign key constraints)
$this->db->getConnection()->exec("DROP TABLE IF EXISTS log");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user");
parent::tearDown();
} }
public function testInsertLog() public function testInsertLog()
{ {
$result = $this->log->insertLog($this->testUserId, 'Test message', 'test'); $result = $this->log->insertLog(1, 'Test message', 'test');
$this->assertTrue($result); $this->assertTrue($result);
// Verify the log was inserted $stmt = $this->db->getConnection()->prepare("SELECT * FROM logs WHERE scope = ?");
$stmt = $this->db->getConnection()->query("SELECT * FROM log WHERE user_id = {$this->testUserId} LIMIT 1"); $stmt->execute(['test']);
$log = $stmt->fetch(PDO::FETCH_ASSOC); $log = $stmt->fetch(PDO::FETCH_ASSOC);
$this->assertEquals(1, $log['user_id']);
$this->assertEquals('Test message', $log['message']); $this->assertEquals('Test message', $log['message']);
$this->assertEquals('test', $log['scope']); $this->assertEquals('test', $log['scope']);
} }
public function testReadLog() public function testReadLog()
{ {
// Insert some test logs with a delay to ensure order // Insert test logs
$this->log->insertLog($this->testUserId, 'Test message 1', 'user'); $this->log->insertLog(1, 'Test message 1', 'user');
sleep(1); // Ensure different timestamps $this->log->insertLog(1, 'Test message 2', 'user');
$this->log->insertLog($this->testUserId, 'Test message 2', 'user');
$logs = $this->log->readLog($this->testUserId, 'user'); $logs = $this->log->readLog(1, 'user');
$this->assertCount(2, $logs); $this->assertCount(2, $logs);
$this->assertEquals('Test message 2', $logs[0]['message']); // Most recent first (by time) $this->assertEquals('Test message 1', $logs[0]['message']);
$this->assertEquals('Test message 2', $logs[1]['message']);
} }
public function testReadLogWithTimeFilter() public function testReadLogWithTimeFilter()
{ {
// First message from yesterday // Insert test logs with different times
$yesterday = date('Y-m-d', strtotime('-1 day')); $this->log->insertLog(1, 'Old message', 'user');
$stmt = $this->db->getConnection()->prepare(" sleep(1); // Ensure different timestamps
INSERT INTO log (user_id, scope, message, time) $this->log->insertLog(1, 'New message', 'user');
VALUES (?, ?, ?, ?)
");
$stmt->execute([$this->testUserId, 'user', 'Test message 1', $yesterday . ' 12:00:00']);
// Second message from today $now = date('Y-m-d H:i:s');
$today = date('Y-m-d'); $oneHourAgo = date('Y-m-d H:i:s', strtotime('-1 hour'));
$stmt->execute([$this->testUserId, 'user', 'Test message 2', $today . ' 12:00:00']);
// Should get only today's messages $logs = $this->log->readLog(1, 'user', 0, '', [
$logs = $this->log->readLog($this->testUserId, 'user', 0, '', [ 'from_time' => $oneHourAgo,
'from_time' => $today 'until_time' => $now
]); ]);
$this->assertCount(1, $logs);
$this->assertEquals('Test message 2', $logs[0]['message']); // Most recent first $this->assertCount(2, $logs);
} }
public function testReadLogWithPagination() public function testReadLogWithPagination()
{ {
// Insert multiple test logs with delays to ensure order // Insert test logs
for ($i = 1; $i <= 5; $i++) { $this->log->insertLog(1, 'Message 1', 'user');
$this->log->insertLog($this->testUserId, "Test message $i", 'user'); $this->log->insertLog(1, 'Message 2', 'user');
sleep(1); // Ensure different timestamps $this->log->insertLog(1, 'Message 3', 'user');
}
// Get all logs to verify total // Test with limit
$allLogs = $this->log->readLog($this->testUserId, 'user'); $logs = $this->log->readLog(1, 'user', 0, 2);
$this->assertCount(5, $allLogs);
// Get first page (offset 0, limit 2)
$logs = $this->log->readLog($this->testUserId, 'user', 0, 2);
$this->assertCount(2, $logs); $this->assertCount(2, $logs);
$this->assertEquals('Test message 5', $logs[0]['message']); // Most recent first
$this->assertEquals('Test message 4', $logs[1]['message']);
// Get second page (offset 2, limit 2) // Test with offset
$logs = $this->log->readLog($this->testUserId, 'user', 2, 2); $logs = $this->log->readLog(1, 'user', 2, 2);
$this->assertCount(2, $logs); $this->assertCount(1, $logs);
$this->assertEquals('Test message 3', $logs[0]['message']);
$this->assertEquals('Test message 2', $logs[1]['message']);
} }
public function testReadLogWithMessageFilter() public function testReadLogWithMessageFilter()
{ {
// Insert test logs with different messages and delays // Insert test logs
$this->log->insertLog($this->testUserId, 'Test message ABC', 'user'); $this->log->insertLog(1, 'Test message', 'user');
sleep(1); // Ensure different timestamps $this->log->insertLog(1, 'Another message', 'user');
$this->log->insertLog($this->testUserId, 'Test message XYZ', 'user');
sleep(1); // Ensure different timestamps
$this->log->insertLog($this->testUserId, 'Different message', 'user');
// Filter by message content $logs = $this->log->readLog(1, 'user', 0, '', [
$logs = $this->log->readLog($this->testUserId, 'user', 0, '', ['message' => 'Test message']); 'message' => 'Test'
$this->assertCount(2, $logs); ]);
// Verify filtered results $this->assertCount(1, $logs);
foreach ($logs as $log) { $this->assertEquals('Test message', $logs[0]['message']);
$this->assertStringContainsString('Test message', $log['message']); }
}
public function testReadLogWithUserFilter()
{
// Insert test logs for different users
$this->log->insertLog(1, 'User 1 message', 'user');
$this->log->insertLog(2, 'User 2 message', 'user');
$logs = $this->log->readLog(1, 'user', 0, '', [
'id' => 1
]);
$this->assertCount(1, $logs);
$this->assertEquals('User 1 message', $logs[0]['message']);
} }
} }

View File

@ -20,26 +20,26 @@ class PlatformTest extends TestCase
'dbFile' => ':memory:' 'dbFile' => ':memory:'
]); ]);
// Create host table // Create hosts table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE host ( CREATE TABLE hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
platform_id INTEGER NOT NULL, platform_id INTEGER NOT NULL,
name TEXT NOT NULL name TEXT NOT NULL
) )
"); ");
// Create jilo_agent table // Create jilo_agents table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE jilo_agent ( CREATE TABLE jilo_agents (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL host_id INTEGER NOT NULL
) )
"); ");
// Create platform table // Create platforms table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE platform ( CREATE TABLE platforms (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
jitsi_url TEXT NOT NULL, jitsi_url TEXT NOT NULL,
@ -64,7 +64,7 @@ class PlatformTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify platform was created // Verify platform was created
$stmt = $this->db->getConnection()->prepare('SELECT * FROM platform WHERE name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM platforms WHERE name = ?');
$stmt->execute([$data['name']]); $stmt->execute([$data['name']]);
$platform = $stmt->fetch(PDO::FETCH_ASSOC); $platform = $stmt->fetch(PDO::FETCH_ASSOC);
@ -77,7 +77,7 @@ class PlatformTest extends TestCase
public function testGetPlatformDetails() public function testGetPlatformDetails()
{ {
// Create test platform // Create test platform
$stmt = $this->db->getConnection()->prepare('INSERT INTO platform (name, jitsi_url, jilo_database) VALUES (?, ?, ?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
$stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']); $stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
$platformId = $this->db->getConnection()->lastInsertId(); $platformId = $this->db->getConnection()->lastInsertId();
@ -95,7 +95,7 @@ class PlatformTest extends TestCase
public function testEditPlatform() public function testEditPlatform()
{ {
// Create test platform // Create test platform
$stmt = $this->db->getConnection()->prepare('INSERT INTO platform (name, jitsi_url, jilo_database) VALUES (?, ?, ?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
$stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']); $stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
$platformId = $this->db->getConnection()->lastInsertId(); $platformId = $this->db->getConnection()->lastInsertId();
@ -109,7 +109,7 @@ class PlatformTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify update // Verify update
$stmt = $this->db->getConnection()->prepare('SELECT * FROM platform WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM platforms WHERE id = ?');
$stmt->execute([$platformId]); $stmt->execute([$platformId]);
$platform = $stmt->fetch(PDO::FETCH_ASSOC); $platform = $stmt->fetch(PDO::FETCH_ASSOC);
@ -120,36 +120,36 @@ class PlatformTest extends TestCase
public function testDeletePlatform() public function testDeletePlatform()
{ {
// Create test platform // Create test platform
$stmt = $this->db->getConnection()->prepare('INSERT INTO platform (name, jitsi_url, jilo_database) VALUES (?, ?, ?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
$stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']); $stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
$platformId = $this->db->getConnection()->lastInsertId(); $platformId = $this->db->getConnection()->lastInsertId();
// Create test host // Create test host
$stmt = $this->db->getConnection()->prepare('INSERT INTO host (platform_id, name) VALUES (?, ?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO hosts (platform_id, name) VALUES (?, ?)');
$stmt->execute([$platformId, 'Test host']); $stmt->execute([$platformId, 'Test host']);
$hostId = $this->db->getConnection()->lastInsertId(); $hostId = $this->db->getConnection()->lastInsertId();
// Create test agent // Create test agent
$stmt = $this->db->getConnection()->prepare('INSERT INTO jilo_agent (host_id) VALUES (?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO jilo_agents (host_id) VALUES (?)');
$stmt->execute([$hostId]); $stmt->execute([$hostId]);
$result = $this->platform->deletePlatform($platformId); $result = $this->platform->deletePlatform($platformId);
$this->assertTrue($result); $this->assertTrue($result);
// Verify platform deletion // Verify platform deletion
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE id = ?');
$stmt->execute([$platformId]); $stmt->execute([$platformId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
$this->assertEquals(0, $result['count']); $this->assertEquals(0, $result['count']);
// Verify host deletion // Verify host deletion
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM host WHERE platform_id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM hosts WHERE platform_id = ?');
$stmt->execute([$platformId]); $stmt->execute([$platformId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
$this->assertEquals(0, $result['count']); $this->assertEquals(0, $result['count']);
// Verify agent deletion // Verify agent deletion
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM jilo_agent WHERE host_id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM jilo_agents WHERE host_id = ?');
$stmt->execute([$hostId]); $stmt->execute([$hostId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
$this->assertEquals(0, $result['count']); $this->assertEquals(0, $result['count']);
@ -167,7 +167,7 @@ class PlatformTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify platform was created // Verify platform was created
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform WHERE name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE name = ?');
$stmt->execute([$validData['name']]); $stmt->execute([$validData['name']]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
$this->assertEquals(1, $result['count']); $this->assertEquals(1, $result['count']);
@ -182,7 +182,7 @@ class PlatformTest extends TestCase
$this->assertIsString($result); // Should return error message $this->assertIsString($result); // Should return error message
// Verify platform was not created // Verify platform was not created
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform WHERE name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE name = ?');
$stmt->execute([$invalidData['name']]); $stmt->execute([$invalidData['name']]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
$this->assertEquals(0, $result['count']); $this->assertEquals(0, $result['count']);
@ -205,7 +205,7 @@ class PlatformTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
// Verify platform was created // Verify platform was created
$stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform WHERE jilo_database = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE jilo_database = ?');
$stmt->execute([$tempDb]); $stmt->execute([$tempDb]);
$result = $stmt->fetch(PDO::FETCH_ASSOC); $result = $stmt->fetch(PDO::FETCH_ASSOC);
$this->assertEquals(1, $result['count']); $this->assertEquals(1, $result['count']);

View File

@ -8,99 +8,103 @@ use PHPUnit\Framework\TestCase;
class RateLimiterTest extends TestCase class RateLimiterTest extends TestCase
{ {
private $db;
private $rateLimiter; private $rateLimiter;
private $db;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
// Prepare DB for Github CI // Set up in-memory SQLite database
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
// Set up test database
$this->db = new Database([ $this->db = new Database([
'type' => 'mariadb', 'type' => 'sqlite',
'host' => $host, 'dbFile' => ':memory:'
'port' => '3306',
'dbname' => 'jilo_test',
'user' => 'test_jilo',
'password' => $password
]); ]);
// The RateLimiter constructor will create all necessary tables
$this->rateLimiter = new RateLimiter($this->db); $this->rateLimiter = new RateLimiter($this->db);
} }
protected function tearDown(): void
{
// Drop tables in correct order
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->authRatelimitTable}");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->pagesRatelimitTable}");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->blacklistTable}");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->whitelistTable}");
parent::tearDown();
}
public function testGetRecentAttempts() public function testGetRecentAttempts()
{ {
$ip = '8.8.8.8'; $ip = '127.0.0.1';
$username = 'testuser';
// Record some login attempts // Clean up any existing attempts first
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->authRatelimitTable} $stmt = $this->db->getConnection()->prepare("DELETE FROM {$this->rateLimiter->authRatelimitTable} WHERE ip_address = ?");
(ip_address, username, attempted_at) VALUES (?, ?, NOW())"); $stmt->execute([$ip]);
// Add 3 attempts
for ($i = 0; $i < 3; $i++) {
$stmt->execute([$ip, 'testuser']);
}
// Initially should have no attempts
$attempts = $this->rateLimiter->getRecentAttempts($ip); $attempts = $this->rateLimiter->getRecentAttempts($ip);
$this->assertEquals(3, $attempts); $this->assertEquals(0, $attempts);
// Add a login attempt
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->authRatelimitTable} (ip_address, username) VALUES (?, ?)");
$stmt->execute([$ip, $username]);
// Should now have 1 attempt
$attempts = $this->rateLimiter->getRecentAttempts($ip);
$this->assertEquals(1, $attempts);
} }
public function testIsIpBlacklisted() public function testIpBlacklisting()
{ {
$ip = '8.8.8.8'; $ip = '192.0.2.1'; // Using TEST-NET-1 range
// Should be blacklisted by default (TEST-NET-1 range)
$this->assertTrue($this->rateLimiter->isIpBlacklisted($ip));
// Test with non-blacklisted IP
$nonBlacklistedIp = '8.8.8.8'; // Google DNS
$this->assertFalse($this->rateLimiter->isIpBlacklisted($nonBlacklistedIp));
// Add IP to blacklist // Add IP to blacklist
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable} $stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable} (ip_address, reason) VALUES (?, ?)");
(ip_address, is_network, reason) VALUES (?, ?, ?)"); $stmt->execute([$nonBlacklistedIp, 'Test blacklist']);
$stmt->execute([$ip, 0, 'Test blacklist']); // Explicitly set is_network to 0 (false)
$this->assertTrue($this->rateLimiter->isIpBlacklisted($ip)); // IP should now be blacklisted
$this->assertFalse($this->rateLimiter->isIpBlacklisted('8.8.4.4')); $this->assertTrue($this->rateLimiter->isIpBlacklisted($nonBlacklistedIp));
} }
public function testIsIpWhitelisted() public function testIpWhitelisting()
{ {
// Test with an IP that's not in the default whitelisted ranges $ip = '127.0.0.1'; // Localhost
$ip = '8.8.8.8'; // Google's DNS, definitely not in private ranges
// Add IP to whitelist // Clean up any existing whitelist entries
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->whitelistTable} $stmt = $this->db->getConnection()->prepare("DELETE FROM {$this->rateLimiter->whitelistTable} WHERE ip_address = ?");
(ip_address, is_network, description) VALUES (?, ?, ?)"); $stmt->execute([$ip]);
$stmt->execute([$ip, 0, 'Test whitelist']); // Explicitly set is_network to 0 (false)
// Add to whitelist
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->whitelistTable} (ip_address, description) VALUES (?, ?)");
$stmt->execute([$ip, 'Test whitelist']);
// Should be whitelisted
$this->assertTrue($this->rateLimiter->isIpWhitelisted($ip)); $this->assertTrue($this->rateLimiter->isIpWhitelisted($ip));
$this->assertFalse($this->rateLimiter->isIpWhitelisted('8.8.4.4')); // Another IP not in private ranges
// Test with non-whitelisted IP
$nonWhitelistedIp = '8.8.8.8'; // Google DNS
$this->assertFalse($this->rateLimiter->isIpWhitelisted($nonWhitelistedIp));
// Add to whitelist
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->whitelistTable} (ip_address, description) VALUES (?, ?)");
$stmt->execute([$nonWhitelistedIp, 'Test whitelist']);
// Should now be whitelisted
$this->assertTrue($this->rateLimiter->isIpWhitelisted($nonWhitelistedIp));
} }
public function testRateLimitCheck() public function testIpRangeBlacklisting()
{ {
$ip = '8.8.8.8'; // Use non-whitelisted IP $ip = '8.8.8.8'; // Google DNS
$endpoint = '/test'; $networkIp = '8.8.8.0/24'; // Network containing Google DNS
// First request should be allowed // Initially IP should not be blacklisted
$this->assertTrue($this->rateLimiter->isPageRequestAllowed($ip, $endpoint)); $this->assertFalse($this->rateLimiter->isIpBlacklisted($ip));
// Add requests up to the limit // Add network to blacklist
for ($i = 0; $i < 60; $i++) { // Default limit is 60 per minute $stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable} (ip_address, is_network, reason) VALUES (?, 1, ?)");
$this->rateLimiter->recordPageRequest($ip, $endpoint); $stmt->execute([$networkIp, 'Test network blacklist']);
}
// The next request should be rate limited // IP in range should now be blacklisted
$this->assertFalse($this->rateLimiter->isPageRequestAllowed($ip, $endpoint)); $this->assertTrue($this->rateLimiter->isIpBlacklisted($ip));
} }
} }

View File

@ -1,91 +0,0 @@
<?php
namespace Tests\Unit\Classes;
use PHPUnit\Framework\TestCase;
class SessionTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
require_once __DIR__ . '/../../../app/classes/session.php';
$_SESSION = [];
}
protected function tearDown(): void
{
parent::tearDown();
$_SESSION = [];
}
public function testGetUsername()
{
$_SESSION['username'] = 'testuser';
$this->assertEquals('testuser', \Session::getUsername());
unset($_SESSION['username']);
$this->assertNull(\Session::getUsername());
}
public function testGetUserId()
{
$_SESSION['user_id'] = 123;
$this->assertEquals(123, \Session::getUserId());
unset($_SESSION['user_id']);
$this->assertNull(\Session::getUserId());
}
public function testIsValidSession()
{
// Invalid without required variables
$this->assertFalse(\Session::isValidSession());
// Valid with required variables
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'testuser';
$_SESSION['LAST_ACTIVITY'] = time();
$this->assertTrue(\Session::isValidSession());
// Invalid after timeout
$_SESSION['LAST_ACTIVITY'] = time() - 8000; // More than 2 hours
$this->assertFalse(\Session::isValidSession());
// Valid with remember me
$_SESSION = [
'user_id' => 123,
'username' => 'testuser',
'REMEMBER_ME' => true,
'LAST_ACTIVITY' => time() - 8000
];
$this->assertTrue(\Session::isValidSession());
}
public function testSetRememberMe()
{
\Session::setRememberMe(true);
$this->assertTrue($_SESSION['REMEMBER_ME']);
\Session::setRememberMe(false);
$this->assertFalse($_SESSION['REMEMBER_ME']);
}
public function test2FASession()
{
// Test storing 2FA pending info
\Session::store2FAPending(123, 'testuser', true);
$this->assertEquals(123, $_SESSION['2fa_pending_user_id']);
$this->assertEquals('testuser', $_SESSION['2fa_pending_username']);
$this->assertTrue(isset($_SESSION['2fa_pending_remember']));
// Test getting 2FA pending info
$pendingInfo = \Session::get2FAPending();
$this->assertEquals([
'user_id' => 123,
'username' => 'testuser',
'remember_me' => true
], $pendingInfo);
// Test clearing 2FA pending info
\Session::clear2FAPending();
$this->assertNull(\Session::get2FAPending());
}
}

View File

@ -1,197 +0,0 @@
<?php
require_once dirname(__DIR__, 3) . '/app/classes/database.php';
require_once dirname(__DIR__, 3) . '/app/classes/user.php';
require_once dirname(__DIR__, 3) . '/plugins/register/models/register.php';
require_once dirname(__DIR__, 3) . '/app/classes/ratelimiter.php';
use PHPUnit\Framework\TestCase;
class UserRegisterTest extends TestCase
{
private $db;
private $register;
private $user;
protected function setUp(): void
{
parent::setUp();
// Prepare DB for Github CI
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
$this->db = new Database([
'type' => 'mariadb',
'host' => $host,
'port' => '3306',
'dbname' => 'jilo_test',
'user' => 'test_jilo',
'password' => $password
]);
// Create user table with MariaDB syntax
$this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
// Create user_meta table with MariaDB syntax
$this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS user_meta (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
name VARCHAR(255),
email VARCHAR(255),
timezone VARCHAR(100),
bio TEXT,
avatar VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
)
");
// Create security_rate_auth table for rate limiting
$this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS security_rate_auth (
id INT PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
username VARCHAR(255) NOT NULL,
attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip_username (ip_address, username)
)
");
// Create user_2fa table for two-factor authentication
$this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS user_2fa (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
secret_key VARCHAR(255) NOT NULL,
backup_codes TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
)
");
$this->register = new Register($this->db);
$this->user = new User($this->db);
}
protected function tearDown(): void
{
// Drop tables in correct order
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user_2fa");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user_meta");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user");
parent::tearDown();
}
public function testRegister()
{
// Register a new user
$username = 'testuser';
$password = 'password123';
$result = $this->register->register($username, $password);
$this->assertTrue($result);
// Verify user was created
$stmt = $this->db->getConnection()->prepare("SELECT * FROM user WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
$this->assertNotNull($user);
$this->assertEquals($username, $user['username']);
$this->assertTrue(password_verify($password, $user['password']));
// Verify metadata was created
$stmt = $this->db->getConnection()->prepare("SELECT * FROM user_meta WHERE user_id = ?");
$stmt->execute([$user['id']]);
$meta = $stmt->fetch(PDO::FETCH_ASSOC);
$this->assertNotNull($meta);
$this->assertEquals($user['id'], $meta['user_id']);
}
public function testLogin()
{
// First register a user
$username = 'testuser';
$password = 'password123';
$this->register->register($username, $password);
// Mock $_SERVER['REMOTE_ADDR'] for rate limiter
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
// Test successful login
try {
$result = $this->user->login($username, $password);
$this->assertIsArray($result);
$this->assertEquals('success', $result['status']);
$this->assertArrayHasKey('user_id', $result);
$this->assertArrayHasKey('username', $result);
$this->assertArrayHasKey('user_id', $_SESSION);
$this->assertArrayHasKey('username', $_SESSION);
$this->assertArrayHasKey('CREATED', $_SESSION);
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
} catch (Exception $e) {
$this->fail('Login should not throw for valid credentials: ' . $e->getMessage());
}
// Test failed login
$result = $this->user->login($username, 'wrongpassword');
$this->assertIsArray($result);
$this->assertEquals('failed', $result['status']);
$this->assertArrayHasKey('message', $result);
$this->assertStringContainsString('Invalid credentials', $result['message']);
}
public function testGetUserDetails()
{
// Register a test user first
$username = 'testuser';
$password = 'password123';
$result = $this->register->register($username, $password);
$this->assertTrue($result);
// Get user ID from database
$stmt = $this->db->getConnection()->prepare("SELECT id FROM user WHERE username = ?");
$stmt->execute([$username]);
$userId = $stmt->fetchColumn();
$this->assertNotFalse($userId);
// Insert user metadata
$stmt = $this->db->getConnection()->prepare("
UPDATE user_meta
SET name = ?, email = ?
WHERE user_id = ?
");
$stmt->execute(['Test User', 'test@example.com', $userId]);
// Get user details
$userDetails = $this->user->getUserDetails($userId);
$this->assertIsArray($userDetails);
$this->assertNotEmpty($userDetails);
$this->assertArrayHasKey(0, $userDetails, 'User details should be returned as an array');
// Get first row since we're querying by primary key
$userDetails = $userDetails[0];
$this->assertArrayHasKey('username', $userDetails, 'User details should include username');
$this->assertArrayHasKey('name', $userDetails, 'User details should include name');
$this->assertArrayHasKey('email', $userDetails, 'User details should include email');
// Verify values
$this->assertEquals($username, $userDetails['username'], 'Username should match');
$this->assertEquals('Test User', $userDetails['name'], 'Name should match');
$this->assertEquals('test@example.com', $userDetails['email'], 'Email should match');
}
}

View File

@ -2,7 +2,6 @@
require_once dirname(__DIR__, 3) . '/app/classes/database.php'; require_once dirname(__DIR__, 3) . '/app/classes/database.php';
require_once dirname(__DIR__, 3) . '/app/classes/user.php'; require_once dirname(__DIR__, 3) . '/app/classes/user.php';
require_once dirname(__DIR__, 3) . '/plugins/register/models/register.php';
require_once dirname(__DIR__, 3) . '/app/classes/ratelimiter.php'; require_once dirname(__DIR__, 3) . '/app/classes/ratelimiter.php';
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -10,161 +9,175 @@ use PHPUnit\Framework\TestCase;
class UserTest extends TestCase class UserTest extends TestCase
{ {
private $db; private $db;
private $register;
private $user; private $user;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
// Prepare DB for Github CI // Set up test database
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
$this->db = new Database([ $this->db = new Database([
'type' => 'mariadb', 'type' => 'sqlite',
'host' => $host, 'dbFile' => ':memory:'
'port' => '3306',
'dbname' => 'jilo_test',
'user' => 'test_jilo',
'password' => $password
]); ]);
// Create user table with MariaDB syntax // Create users table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS user ( CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(255) NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password TEXT NOT NULL
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
"); ");
// Create user_meta table with MariaDB syntax // Create users_meta table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS user_meta ( CREATE TABLE users_meta (
id INT PRIMARY KEY AUTO_INCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INT NOT NULL, user_id INTEGER NOT NULL,
name VARCHAR(255), name TEXT,
email VARCHAR(255), email TEXT,
timezone VARCHAR(100), timezone TEXT,
bio TEXT, bio TEXT,
avatar VARCHAR(255), avatar TEXT,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(id)
)
");
// Create security_rate_auth table for rate limiting
$this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS security_rate_auth (
id INT PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
username VARCHAR(255) NOT NULL,
attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip_username (ip_address, username)
) )
"); ");
// Create user_2fa table for two-factor authentication // Create user_2fa table for two-factor authentication
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE IF NOT EXISTS user_2fa ( CREATE TABLE user_2fa (
id INT PRIMARY KEY AUTO_INCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INT NOT NULL, user_id INTEGER NOT NULL,
secret_key VARCHAR(255) NOT NULL, secret_key TEXT NOT NULL,
backup_codes TEXT, backup_codes TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 0, enabled TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
");
// Create tables for rate limiter
$this->db->getConnection()->exec("
CREATE TABLE login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now'))
)
");
$this->db->getConnection()->exec("
CREATE TABLE ip_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description TEXT,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
)
");
$this->db->getConnection()->exec("
CREATE TABLE ip_blacklist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT,
expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
) )
"); ");
$this->user = new User($this->db); $this->user = new User($this->db);
$this->register = new Register($this->db);
} }
protected function tearDown(): void public function testRegister()
{ {
// Drop tables in correct order $result = $this->user->register('testuser', 'password123');
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user_2fa"); $this->assertTrue($result);
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user_meta"); // Verify user was created
$this->db->getConnection()->exec("DROP TABLE IF EXISTS user"); $stmt = $this->db->getConnection()->prepare('SELECT * FROM users WHERE username = ?');
parent::tearDown(); $stmt->execute(['testuser']);
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
$this->assertEquals('testuser', $user['username']);
$this->assertTrue(password_verify('password123', $user['password']));
// Verify user_meta was created
$stmt = $this->db->getConnection()->prepare('SELECT * FROM users_meta WHERE user_id = ?');
$stmt->execute([$user['id']]);
$meta = $stmt->fetch(\PDO::FETCH_ASSOC);
$this->assertNotNull($meta);
} }
public function testLogin() public function testLogin()
{ {
// First register a user // Create a test user
$username = 'testuser';
$password = 'password123'; $password = 'password123';
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$this->register->register($username, $password); $stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
$stmt->execute(['testuser', $hashedPassword]);
// Mock $_SERVER['REMOTE_ADDR'] for rate limiter // Mock $_SERVER['REMOTE_ADDR'] for rate limiter
$_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
// Test successful login // Test successful login
try { try {
$result = $this->user->login($username, $password); $result = $this->user->login('testuser', $password);
$this->assertIsArray($result); $this->assertIsArray($result);
$this->assertEquals('success', $result['status']); $this->assertEquals('success', $result['status']);
$this->assertArrayHasKey('user_id', $result); $this->assertArrayHasKey('user_id', $result);
$this->assertArrayHasKey('username', $result); $this->assertArrayHasKey('username', $result);
$this->assertArrayHasKey('user_id', $_SESSION); $this->assertArrayHasKey('user_id', $_SESSION);
$this->assertArrayHasKey('username', $_SESSION);
$this->assertArrayHasKey('CREATED', $_SESSION); $this->assertArrayHasKey('CREATED', $_SESSION);
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION); $this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
} catch (Exception $e) { } catch (Exception $e) {
$this->fail('Login should not throw for valid credentials: ' . $e->getMessage()); $this->fail('Login should not throw an exception for valid credentials: ' . $e->getMessage());
} }
// Test failed login // Test failed login
$result = $this->user->login($username, 'wrongpassword'); try {
$this->assertIsArray($result); $this->user->login('testuser', 'wrongpassword');
$this->assertEquals('failed', $result['status']); $this->fail('Login should throw an exception for invalid credentials');
$this->assertArrayHasKey('message', $result); } catch (Exception $e) {
$this->assertStringContainsString('Invalid credentials', $result['message']); $this->assertStringContainsString('Invalid credentials', $e->getMessage());
}
// Test nonexistent user
try {
$this->user->login('nonexistent', $password);
$this->fail('Login should throw an exception for nonexistent user');
} catch (Exception $e) {
$this->assertStringContainsString('Invalid credentials', $e->getMessage());
}
} }
public function testGetUserDetails() public function testGetUserDetails()
{ {
// Register a test user first // Create a test user
$username = 'testuser'; $stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
$password = 'password123'; $stmt->execute(['testuser', 'hashedpassword']);
$result = $this->register->register($username, $password); $userId = $this->db->getConnection()->lastInsertId();
$this->assertTrue($result);
// Get user ID from database // Create user meta with some data
$stmt = $this->db->getConnection()->prepare("SELECT id FROM user WHERE username = ?"); $stmt = $this->db->getConnection()->prepare('INSERT INTO users_meta (user_id, name, email) VALUES (?, ?, ?)');
$stmt->execute([$username]); $stmt->execute([$userId, 'Test User', 'test@example.com']);
$userId = $stmt->fetchColumn();
$this->assertNotFalse($userId);
// Insert user metadata
$stmt = $this->db->getConnection()->prepare("
UPDATE user_meta
SET name = ?, email = ?
WHERE user_id = ?
");
$stmt->execute(['Test User', 'test@example.com', $userId]);
// Get user details
$userDetails = $this->user->getUserDetails($userId); $userDetails = $this->user->getUserDetails($userId);
$this->assertIsArray($userDetails); $this->assertIsArray($userDetails);
$this->assertNotEmpty($userDetails); $this->assertCount(1, $userDetails); // Should return one row
$this->assertArrayHasKey(0, $userDetails, 'User details should be returned as an array'); $user = $userDetails[0]; // Get the first row
$this->assertEquals('testuser', $user['username']);
$this->assertEquals('Test User', $user['name']);
$this->assertEquals('test@example.com', $user['email']);
// Get first row since we're querying by primary key // Test nonexistent user
$userDetails = $userDetails[0]; $userDetails = $this->user->getUserDetails(999);
$this->assertEmpty($userDetails);
$this->assertArrayHasKey('username', $userDetails, 'User details should include username');
$this->assertArrayHasKey('name', $userDetails, 'User details should include name');
$this->assertArrayHasKey('email', $userDetails, 'User details should include email');
// Verify values
$this->assertEquals($username, $userDetails['username'], 'Username should match');
$this->assertEquals('Test User', $userDetails['name'], 'Name should match');
$this->assertEquals('test@example.com', $userDetails['email'], 'Email should match');
} }
} }

View File

@ -12,19 +12,9 @@ if (!headers_sent()) {
ini_set('session.gc_maxlifetime', 1440); // 24 minutes ini_set('session.gc_maxlifetime', 1440); // 24 minutes
} }
// Load plugin Log model and IP helper early so fallback wrapper is bypassed
require_once __DIR__ . '/../app/helpers/ip_helper.php';
// Initialize global user_IP for tests
global $user_IP;
$user_IP = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
// Load Composer's autoloader // Load Composer's autoloader
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
// Ensure core NullLogger is available during tests
require_once __DIR__ . '/../app/core/NullLogger.php';
// Set error reporting // Set error reporting
error_reporting(E_ALL); error_reporting(E_ALL);
ini_set('display_errors', 1); ini_set('display_errors', 1);
@ -36,23 +26,18 @@ date_default_timezone_set('UTC');
// Define global variables needed by the application // Define global variables needed by the application
$GLOBALS['app_root'] = '/'; $GLOBALS['app_root'] = '/';
$GLOBALS['config'] = [ $GLOBALS['config'] = [
'db_type' => 'mariadb', 'db' => [
'sql' => [ 'type' => 'sqlite',
'sql_host' => 'localhost', 'dbFile' => ':memory:'
'sql_port' => '3306', ]
'sql_database' => 'jilo_test',
'sql_username' => 'test_jilo',
'sql_password' => '',
],
'environment' => 'testing'
]; ];
// Define global connectDB function // Define global connectDB function
if (!function_exists('connectDB')) { if (!function_exists('connectDB')) {
function connectDB($config) { function connectDB($config) {
global $db; global $dbWeb;
return [ return [
'db' => $db 'db' => $dbWeb
]; ];
} }
} }

View File

@ -26,11 +26,7 @@
</coverage> </coverage>
<php> <php>
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="DB_TYPE" value="mariadb"/> <env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_HOST" value="localhost"/> <env name="DB_DATABASE" value=":memory:"/>
<env name="DB_PORT" value="3306"/>
<env name="DB_DATABASE" value="jilo_test"/>
<env name="DB_USERNAME" value="root"/>
<env name="DB_PASSWORD" value=""/>
</php> </php>
</phpunit> </phpunit>

View File

@ -1,35 +0,0 @@
-- Create test database if not exists
CREATE DATABASE IF NOT EXISTS jilo_test;
USE jilo_test;
-- Create rate limiter table if not exists
CREATE TABLE IF NOT EXISTS security_rate_page (
id INT PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
endpoint VARCHAR(255) NOT NULL,
request_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create security_ip_whitelist table if not exists
CREATE TABLE IF NOT EXISTS security_ip_whitelist (
id INT PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255)
);
-- Create security_ip_blacklist table if not exists
CREATE TABLE IF NOT EXISTS security_ip_blacklist (
id INT PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0,
reason TEXT,
expiry_time TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255)
);
-- Grant permissions to root user
GRANT ALL PRIVILEGES ON jilo_test.* TO 'root'@'localhost';

View File

@ -1,12 +0,0 @@
<?php
return [
'name' => 'Modern theme',
'description' => 'Example theme. A modern, clean theme for Jilo Web.',
'version' => '1.0.0',
'author' => 'Lindeas Inc.',
'screenshot' => 'screenshot.png',
'options' => [
// Theme-specific options can be defined here
]
];

View File

@ -1,180 +0,0 @@
/* Modern Theme Styles */
:root {
--primary-color: #4361ee;
--secondary-color: #3f37c9;
--accent-color: #4895ef;
--light-color: #f8f9fa;
--dark-color: #212529;
--success-color: #4bb543;
--warning-color: #f9c74f;
--danger-color: #ef476f;
--border-radius: 0.5rem;
}
/* General Styles */
body.modern-theme {
background-color: #f5f7fa;
color: #333;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Navigation */
.menu-container {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 0.5rem 1rem;
}
.menu-left {
display: flex;
align-items: center;
}
.menu-right {
display: flex;
align-items: center;
justify-content: flex-end;
}
/* Cards */
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
margin-bottom: 1.5rem;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: white;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
font-weight: 600;
}
/* Buttons */
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
border-radius: var(--border-radius);
padding: 0.375rem 1rem;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: var(--secondary-color);
border-color: var(--secondary-color);
transform: translateY(-1px);
}
/* Forms */
.form-control {
border-radius: var(--border-radius);
border: 1px solid #e1e5eb;
padding: 0.5rem 0.75rem;
}
.form-control:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 0.2rem rgba(67, 97, 238, 0.25);
}
/* Tables */
.table {
background-color: white;
border-radius: var(--border-radius);
overflow: hidden;
}
.table thead th {
background-color: #f8f9fa;
border-bottom: 2px solid #e9ecef;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.5px;
}
/* Alerts */
.alert {
border-radius: var(--border-radius);
border: none;
padding: 1rem 1.25rem;
}
.alert-success {
background-color: rgba(75, 181, 67, 0.1);
color: var(--success-color);
}
.alert-warning {
background-color: rgba(249, 199, 79, 0.1);
color: #c9a227;
}
.alert-danger {
background-color: rgba(239, 71, 111, 0.1);
color: var(--danger-color);
}
/* Theme Switcher */
.theme-switcher {
margin-left: 1rem;
}
.theme-switcher .dropdown-toggle {
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: white;
transition: all 0.2s;
}
.theme-switcher .dropdown-toggle:hover {
background: rgba(255, 255, 255, 0.2);
}
.theme-option {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.theme-option:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.theme-preview {
width: 20px;
height: 20px;
border-radius: 4px;
margin-right: 10px;
border: 1px solid rgba(0, 0, 0, 0.1);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.menu-container {
flex-direction: column;
padding: 0.5rem;
}
.menu-left, .menu-right {
width: 100%;
justify-content: space-between;
padding: 0.25rem 0;
}
}

Some files were not shown because too many files have changed in this diff Show More