Compare commits

..

116 Commits
v0.4 ... HEAD

Author SHA1 Message Date
Yasen Pramatarov 3e72568141 Fixes theme helper 2025-06-26 14:48:51 +03:00
Yasen Pramatarov 1993a7e2de Fixes themes footers 2025-06-26 14:48:10 +03:00
Yasen Pramatarov 91cabf56e7 Adds theme-asset page in index 2025-06-26 14:27:54 +03:00
Yasen Pramatarov aea05ce0e6 Fixes theme asset helper 2025-06-26 14:27:15 +03:00
Yasen Pramatarov 85a489244d Adds modern theme screenshot 2025-06-26 14:13:19 +03:00
Yasen Pramatarov 37b65897df Adds retro theme screenshot 2025-06-26 14:10:41 +03:00
Yasen Pramatarov 361a1e6c6d Troubleshoots theme helper 2025-06-23 14:08:11 +03:00
Yasen Pramatarov 51f0b0b369 Fiexs theme view to use the new theme assets code 2025-06-23 14:01:29 +03:00
Yasen Pramatarov 30ea7ff5c0 Removes getScreenshotUrl, we now use generic getAssetUrl for all assets 2025-06-22 13:58:17 +03:00
Yasen Pramatarov 55829faf85 Fixes theme helper to use the new theme assets helper 2025-06-20 13:55:08 +03:00
Yasen Pramatarov 37566b5122 Adds helper to manage all static assets of a theme 2025-06-19 13:47:09 +03:00
Yasen Pramatarov 8203c10f37 Adds theme data to be passed to the views 2025-06-19 13:45:14 +03:00
Yasen Pramatarov b1dae54aac Fixes templates loading in index 2025-06-18 12:34:56 +03:00
Yasen Pramatarov d65b7bcc55 Fixes index to work with latest session and config changes 2025-06-17 12:23:13 +03:00
Yasen Pramatarov a0f3e84432 Fixes theme helper to have config array always available 2025-06-16 12:22:26 +03:00
Yasen Pramatarov c9490cf149 Troubleshoots router class 2025-06-13 12:20:37 +03:00
Yasen Pramatarov ad8c833862 Refactors session, adds random session name if not configured 2025-06-11 12:18:48 +03:00
Yasen Pramatarov 47875289a8 Bugfixes theme switcher 2025-06-10 11:55:06 +03:00
Yasen Pramatarov e544176cdd Bugfixes session class 2025-06-08 11:52:53 +03:00
Yasen Pramatarov 65f9c4da3c Moves session variables to config file 2025-06-06 11:51:09 +03:00
Yasen Pramatarov bdcc308188 Adds CSRF the theme switcher 2025-06-03 11:01:14 +03:00
Yasen Pramatarov 06f8229a8c Bugfixes theme switcher 2025-06-01 10:39:39 +03:00
Yasen Pramatarov 1c93864567 Adds alternative test theme 2025-05-29 10:23:43 +03:00
Yasen Pramatarov 522d84f203 Bugfixes theme changer page 2025-05-28 10:16:12 +03:00
Yasen Pramatarov 6617b3bb28 Updates changelog 2025-05-27 10:10:04 +03:00
Yasen Pramatarov c4800f4943 Adds js to default theme 2025-05-26 10:08:46 +03:00
Yasen Pramatarov f0820b05c2 Adds CSS to default theme 2025-05-25 10:07:27 +03:00
Yasen Pramatarov 1b2ff95c1e Adds theme item in profile menu 2025-05-24 09:59:56 +03:00
Yasen Pramatarov 4867df89a1 Bugfixes theme helper 2025-05-23 09:58:50 +03:00
Yasen Pramatarov 4715a26af7 Initialize themes system 2025-05-22 15:13:23 +03:00
Yasen Pramatarov d366c1dd10 Makes index dynamically load theme's templates 2025-05-22 14:39:20 +03:00
Yasen Pramatarov 2d0c280a0a Adds theme switcher controller and view 2025-05-22 14:33:54 +03:00
Yasen Pramatarov c2cfd503ee Adds theme-related feedback messages 2025-05-22 14:20:36 +03:00
Yasen Pramatarov 6c8806965e Adds header and footer for the example "modern" theme 2025-05-20 15:20:28 +03:00
Yasen Pramatarov 8745c0598f Adds an example theme "modern" 2025-05-19 15:05:07 +03:00
Yasen Pramatarov 69b40ca560 Adds theme config, prepares for themes system 2025-05-18 15:01:05 +03:00
Yasen Pramatarov c53591e9ec Adds theme helper, prepares for themes system 2025-05-15 14:54:21 +03:00
Yasen Pramatarov 24c844db49 Allows superuser to load settigs page 2025-05-08 20:07:53 +03:00
Yasen Pramatarov 65f0758e82 Updates logs plugin to new pages system, bumps version 2025-05-08 19:38:52 +03:00
Yasen Pramatarov 2f4b0b7aef Updates register plugin to new pages system, bumps version 2025-05-08 19:38:21 +03:00
Yasen Pramatarov 36e81104f1 Refactors index.php to allow multiple pages per plugin 2025-05-08 19:37:35 +03:00
Yasen Pramatarov fd835dd058 Fixes log levels displaying 2025-04-27 21:38:24 +03:00
Yasen Pramatarov d886bcf755 Adds proper log levels to log plugin 2025-04-27 21:20:14 +03:00
Yasen Pramatarov 81b4187ae8 Removes obsolete insertLog() 2025-04-27 19:21:23 +03:00
Yasen Pramatarov bbccb54059 Updates log calls to new syntax 2025-04-27 19:00:58 +03:00
Yasen Pramatarov 1e975f7b18 Makes old code work with the new Log plugin 2025-04-27 15:55:35 +03:00
Yasen Pramatarov 457c946946 Adds some user right restrictions 2025-04-27 15:48:07 +03:00
Yasen Pramatarov f84a337607 Fixes log plugin 2025-04-27 15:43:45 +03:00
Yasen Pramatarov fa3e75f722 Fixes log plugin 2025-04-27 15:41:01 +03:00
Yasen Pramatarov 1f3d331b25 Adds pipeline in index for all middleware 2025-04-26 15:36:41 +03:00
Yasen Pramatarov 315fbcb18f Updates github workflow after mariadb migration 2025-04-25 18:42:28 +03:00
Yasen Pramatarov 6fdf123f9f Fixes tests 2025-04-25 18:30:24 +03:00
Yasen Pramatarov 4a43d8cfc7 Fixes tests 2025-04-25 17:15:56 +03:00
Yasen Pramatarov adb8e42d61 Changes sql tables to use singular names 2025-04-25 16:16:38 +03:00
Yasen Pramatarov 880c45025c Migrates app database from SQLite to MariaDB 2025-04-25 12:10:29 +03:00
Yasen Pramatarov 630f71ce4d Fixes SQL syntax typos 2025-04-25 11:22:20 +03:00
Yasen Pramatarov e8576d3e94 Removes logging logic from index and replaces old log class with a wrapper 2025-04-25 10:20:57 +03:00
Yasen Pramatarov ff28ebf753 Adds log entry to main menu 2025-04-25 10:14:21 +03:00
Yasen Pramatarov 242b63317b Makes the old code work with the new Log plugin 2025-04-25 10:13:12 +03:00
Yasen Pramatarov a004602ce2 Replaces olg logs helper with IP helper 2025-04-25 10:07:01 +03:00
Yasen Pramatarov c749726a79 Removes old log pages 2025-04-25 10:05:51 +03:00
Yasen Pramatarov 761c27c0d3 Adds Log plugin 2025-04-25 10:02:49 +03:00
Yasen Pramatarov fe91a91081 Adds NullLogger when logging system is missing 2025-04-25 09:52:48 +03:00
Yasen Pramatarov 0447439f99 Adds SQL file(preparation for sqlite->mariadb migration) 2025-04-25 09:48:43 +03:00
Yasen Pramatarov ed1c305358 Fixes config code 2025-04-24 14:30:35 +03:00
Yasen Pramatarov 13f2ca4fe4 Moves database connection to DatabaseConnector core class 2025-04-24 14:12:24 +03:00
Yasen Pramatarov 891e85b0bb Adds DatabaseConnector core class 2025-04-24 13:57:08 +03:00
Yasen Pramatarov c2f0fe6793 Moves config loading to ConfigLoader core class 2025-04-24 13:52:37 +03:00
Yasen Pramatarov 7dfbe49996 Adds ConfigLoader core class 2025-04-24 13:49:52 +03:00
Yasen Pramatarov ed0baf18d3 Moves plugin disovery and hooks code away from index 2025-04-24 13:32:45 +03:00
Yasen Pramatarov 8628985361 Adds HookDispatcher core class for plugin hooks 2025-04-24 13:25:30 +03:00
Yasen Pramatarov facddb0d6d Adds PluginManager core class 2025-04-24 13:25:03 +03:00
Yasen Pramatarov 9797caa58e Moves session auth logic to the new core/Router class 2025-04-24 12:37:59 +03:00
Yasen Pramatarov 9c896d9e0e Creates /app/core/ for core infrastructure code, essential for the app 2025-04-24 12:34:27 +03:00
Yasen Pramatarov bccd48014b Removes no more used session middleware 2025-04-23 13:58:43 +03:00
Yasen Pramatarov 10083ff7af Adds 'logout' and 'register' to login redirect whitelist 2025-04-23 13:57:11 +03:00
Yasen Pramatarov cfa8540be9 Removes username forms autofocus 2025-04-23 13:56:07 +03:00
Yasen Pramatarov 3657dd70cf Fixes session timeout and login issues 2025-04-23 13:54:59 +03:00
Yasen Pramatarov e88229bee2 Troubleshoots login redirects 2025-04-22 16:20:56 +03:00
Yasen Pramatarov 0b59072d9b Adds login redirection to original requested page 2025-04-22 15:31:50 +03:00
Yasen Pramatarov 6542df9074 Fixes the tests 2025-04-17 10:59:40 +03:00
Yasen Pramatarov 40c646291e Removes old registration core code 2025-04-17 10:45:29 +03:00
Yasen Pramatarov 4877354e8d Fixes register plugin 2025-04-17 10:41:40 +03:00
Yasen Pramatarov 61d23cd8c2 Lets plugins add themselves to the public pages 2025-04-17 10:36:45 +03:00
Yasen Pramatarov 8dfd54eb9f Replaces hardcoded register link with a plugin hook 2025-04-17 10:31:35 +03:00
Yasen Pramatarov af8d86321f Removes hardcoded "register" page 2025-04-17 10:30:34 +03:00
Yasen Pramatarov 26817c1bb6 Adds registration plugin 2025-04-17 10:29:31 +03:00
Yasen Pramatarov 6443eb9b00 Makes plugin system plugin-name agnostic 2025-04-17 10:20:37 +03:00
Yasen Pramatarov 14eefb99e9 Adds "manage plugins" right 2025-04-17 09:46:19 +03:00
Yasen Pramatarov 3915ca6633 Prepares for plugins. Autodiscovery and hooks. 2025-04-16 20:23:27 +03:00
Yasen Pramatarov 5246c47ee6 Makes csrf_token a global constant and moves it to includes 2025-04-16 13:11:51 +03:00
Yasen Pramatarov 221a6e8139 Removes system settings entries from sidebar menu 2025-04-15 22:43:23 +03:00
Yasen Pramatarov b098096930 Uses $userId instead of session var 2025-04-15 22:40:29 +03:00
Yasen Pramatarov 47779baa5e Adds top right system menu 2025-04-15 22:37:49 +03:00
Yasen Pramatarov eebdbc409c Adds top right help menu 2025-04-15 22:29:55 +03:00
Yasen Pramatarov 95530ed5f0 Adds CSRF to profile edit pages 2025-04-15 18:10:17 +03:00
Yasen Pramatarov 0a7f3737c5 Explicitly adds/removes rights, makes possible to remove all rights 2025-04-15 18:05:09 +03:00
Yasen Pramatarov 9cb7812144 Bugfixes 2025-04-15 17:57:13 +03:00
Yasen Pramatarov 4625321079 Removes length check for old password 2025-04-14 19:39:51 +03:00
Yasen Pramatarov 1c2c1a76fa Fixes bugs in login ratelimiting 2025-04-14 19:36:07 +03:00
Yasen Pramatarov 8d64bf7c6e Ratelimits only failed login attempts 2025-04-14 19:12:26 +03:00
Yasen Pramatarov 45181c11c5 Fixes db connection issues 2025-04-14 18:07:15 +03:00
Yasen Pramatarov e96480807c Updates SQL schemas 2025-04-14 18:06:44 +03:00
Yasen Pramatarov 9e94639657 Makes password at least 8 chars 2025-04-14 15:48:54 +03:00
Yasen Pramatarov 649a94c560 Fixes to show session expiration only once 2025-04-14 15:31:19 +03:00
Yasen Pramatarov 8655258ac3 Standartizes $userId as user ID variable in whole app 2025-04-14 10:39:58 +03:00
Yasen Pramatarov 67ba6b38c7 Session expiration bug fix 2025-04-14 10:06:13 +03:00
Yasen Pramatarov 16854f0f77 Fixes tests and adds Session unit test 2025-04-13 20:51:52 +03:00
Yasen Pramatarov 582b5492fe Removes unneded login reirects 2025-04-13 20:05:10 +03:00
Yasen Pramatarov 101f4c539a Validates pagination vars 2025-04-13 19:49:47 +03:00
Yasen Pramatarov 522cded113 Implements the session class 2025-04-13 19:46:48 +03:00
Yasen Pramatarov f77e15bf44 Implements the new session class 2025-04-13 19:34:13 +03:00
Yasen Pramatarov dbdbe1bf49 Switches to session class in templates 2025-04-13 19:12:28 +03:00
Yasen Pramatarov d3f0c90272 Removes code duplicating with session class 2025-04-13 19:11:52 +03:00
Yasen Pramatarov 566b16190e Adds session timeout message 2025-04-13 19:06:48 +03:00
Yasen Pramatarov 5281102e36 Adds a special 'session' class for all session things. 2025-04-13 15:18:53 +03:00
110 changed files with 4742 additions and 1571 deletions

View File

@ -10,21 +10,103 @@ 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.1' php-version: '8.2'
extensions: pdo, pdo_sqlite extensions: pdo, pdo_mysql, xdebug
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,6 +13,8 @@ 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_agents ja jilo_agent ja
JOIN JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN JOIN
hosts h ON ja.host_id = h.id host 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_agents ja jilo_agent ja
JOIN JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN JOIN
hosts h ON ja.host_id = h.id host 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_types FROM jilo_agent_type
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_agents jilo_agent
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_agents $sql = 'INSERT INTO jilo_agent
(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_agents $sql = 'UPDATE jilo_agent
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_agents $sql = 'DELETE FROM jilo_agent
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_checks jac jilo_agent_check jac
JOIN JOIN
jilo_agents ja ON jac.agent_id = ja.id jilo_agent ja ON jac.agent_id = ja.id
JOIN JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN JOIN
hosts h ON ja.host_id = h.id host 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_checks jac jilo_agent_check jac
JOIN JOIN
jilo_agents ja ON jac.agent_id = ja.id jilo_agent ja ON jac.agent_id = ja.id
JOIN JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN JOIN
hosts h ON ja.host_id = h.id host 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_checks jac jilo_agent_check jac
JOIN JOIN
jilo_agents ja ON jac.agent_id = ja.id jilo_agent ja ON jac.agent_id = ja.id
JOIN JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN JOIN
hosts h ON ja.host_id = h.id host 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->insertLog(0, "Retrieved " . count($result) . " Jitsi component events"); $logObject->log('info', "Retrieved " . count($result) . " Jitsi component events", ['user_id' => $userId, 'scope' => 'system']);
} }
return $result; return $result;
} catch (PDOException $e) { } catch (PDOException $e) {
$logObject->insertLog(0, "Failed to retrieve Jitsi component events: " . $e->getMessage()); $logObject->log('error', "Failed to retrieve Jitsi component events: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
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->insertLog(0, "Failed to retrieve component events count: " . $e->getMessage()); $logObject->log('error', "Failed to retrieve component events count: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
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, $user_id; global $logObject, $userId;
$allLogs = []; $allLogs = [];
$updated = []; $updated = [];
@ -140,7 +140,7 @@ class Config {
} }
if (!empty($allLogs)) { if (!empty($allLogs)) {
$logObject->insertLog($user_id, implode("\n", $allLogs), 'system'); $logObject->log('info', implode("\n", $allLogs), ['user_id' => $userId, 'scope' => 'system']);
} }
return [ return [
@ -148,7 +148,7 @@ class Config {
'updated' => $updated 'updated' => $updated
]; ];
} catch (Exception $e) { } catch (Exception $e) {
$logObject->insertLog($user_id, "Config update error: " . $e->getMessage(), 'system'); $logObject->log('error', "Config update error: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
return [ return [
'success' => false, 'success' => false,
'error' => $e->getMessage() 'error' => $e->getMessage()

View File

@ -35,6 +35,10 @@ 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
@ -49,6 +53,21 @@ 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,
@ -96,19 +115,15 @@ class Feedback {
] ]
]; ];
const REGISTER = [ const THEME = [
'SUCCESS' => [ 'THEME_CHANGE_SUCCESS' => [
'type' => self::TYPE_SUCCESS, 'type' => self::TYPE_SUCCESS,
'dismissible' => true 'dismissible' => true
], ],
'FAILED' => [ 'THEME_CHANGE_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
hosts'; host';
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 hosts $sql = 'INSERT INTO host
(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 hosts SET $sql = 'UPDATE host 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_agents WHERE host_id = :host_id'; $sql = 'DELETE FROM jilo_agent 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 hosts WHERE id = :host_id'; $sql = 'DELETE FROM host 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,122 +1,40 @@
<?php <?php
/** /**
* class Log * Log wrapper that delegates to plugin Log or NullLogger fallback.
* * 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;
/** /**
* Logs constructor. * @param mixed $database Database or DatabaseConnector instance
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/ */
public function __construct($database) { public function __construct($database) {
$this->db = $database->getConnection(); global $logObject;
} if (isset($logObject) && method_exists($logObject, 'insertLog')) {
$this->logger = $logObject;
/** } else {
* Insert a log event into the database. $this->logger = new \App\Core\NullLogger();
*
* @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();
} }
} }
/** /**
* Retrieve log entries from the database. * PSR-3 compatible log method
* * @param string $level
* @param int $user_id The ID of the user whose logs are being retrieved. * @param string $message
* @param string $scope The scope of the logs ('user' or 'system'). * @param array $context
* @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($user_id, $scope, $offset=0, $items_per_page='', $filters=[]) { public function log(string $level, string $message, array $context = []): void {
$params = []; $this->logger->log($level, $message, $context);
$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 users u FROM user u
JOIN users_meta um ON u.id = um.user_id JOIN user_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 platforms'; $sql = 'SELECT * FROM platform';
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 platforms $sql = 'INSERT INTO platform
(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 platforms SET $sql = 'UPDATE platform 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 hosts WHERE platform_id = :platform_id'; $sql = 'SELECT id FROM host 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_agents WHERE host_id = :host_id'; $sql = 'DELETE FROM jilo_agent 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 hosts WHERE platform_id = :platform_id'; $sql = 'DELETE FROM host 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 platforms WHERE id = :platform_id'; $sql = 'DELETE FROM platform 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,16 +1,20 @@
<?php <?php
use App\Core\NullLogger;
class RateLimiter { class RateLimiter {
public $db; public $db;
private $log; private $database;
/** @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 = 'login_attempts'; // For username/password attempts public $authRatelimitTable = 'security_rate_auth'; // For rate limiting username/password attempts
public $pagesRatelimitTable = 'pages_rate_limits'; // For page requests public $pagesRatelimitTable = 'security_rate_page'; // For rate limiting page requests
public $whitelistTable = 'ip_whitelist'; public $whitelistTable = 'security_ip_whitelist'; // For whitelisting IPs and network ranges
public $blacklistTable = 'ip_blacklist'; public $blacklistTable = 'security_ip_blacklist'; // For blacklisting IPs and network ranges
private $pageLimits = [ private $pageLimits = [
// Default rate limits per minute // Default rate limits per minute
'default' => 60, 'default' => 60,
@ -22,9 +26,23 @@ 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();
$this->log = new Log($database); // Initialize logger (plugin Log if present or NullLogger otherwise)
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();
} }
@ -32,42 +50,47 @@ 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 INTEGER PRIMARY KEY AUTOINCREMENT, id int(11) PRIMARY KEY AUTO_INCREMENT,
ip_address TEXT NOT NULL, ip_address VARCHAR(45) NOT NULL,
username TEXT NOT NULL, username VARCHAR(255) NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now')) attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
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 INTEGER PRIMARY KEY AUTOINCREMENT, id int(11) PRIMARY KEY AUTO_INCREMENT,
ip_address TEXT NOT NULL, ip_address VARCHAR(45) NOT NULL,
endpoint TEXT NOT NULL, endpoint VARCHAR(255) 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 INTEGER PRIMARY KEY AUTOINCREMENT, id int(11) PRIMARY KEY AUTO_INCREMENT,
ip_address TEXT NOT NULL UNIQUE, ip_address VARCHAR(45) NOT NULL,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)), is_network BOOLEAN DEFAULT FALSE,
description TEXT, description VARCHAR(255),
created_at TEXT DEFAULT (DATETIME('now')), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT created_by VARCHAR(255),
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 INTEGER PRIMARY KEY AUTOINCREMENT, id int(11) PRIMARY KEY AUTO_INCREMENT,
ip_address TEXT NOT NULL UNIQUE, ip_address VARCHAR(45) NOT NULL,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)), is_network BOOLEAN DEFAULT FALSE,
reason TEXT, reason VARCHAR(255),
expiry_time TEXT NULL, expiry_time TIMESTAMP NULL,
created_at TEXT DEFAULT (DATETIME('now')), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT created_by VARCHAR(255),
UNIQUE KEY unique_ip (ip_address)
)"; )";
$this->db->exec($sql); $this->db->exec($sql);
@ -81,7 +104,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 OR IGNORE INTO {$this->whitelistTable} $stmt = $this->db->prepare("INSERT 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) {
@ -97,7 +120,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 OR IGNORE INTO {$this->blacklistTable} $stmt = $this->db->prepare("INSERT IGNORE INTO {$this->blacklistTable}
(ip_address, is_network, reason, created_by) (ip_address, is_network, reason, created_by)
VALUES (?, ?, ?, 'system')"); VALUES (?, ?, ?, 'system')");
@ -112,7 +135,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 > datetime('now', '-' || :minutes || ' minutes')"); WHERE ip_address = ? AND attempted_at > DATE_SUB(NOW(), INTERVAL ? MINUTE)");
$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']);
@ -209,15 +232,19 @@ 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->log->insertLog($userId, "IP Whitelist: {$message}", 'system'); $this->logger->log('info', "IP Whitelist: {$message}", ['user_id' => $userId, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', $message); Feedback::flash('ERROR', 'DEFAULT', $message);
} }
return false; return false;
} }
$stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->whitelistTable} $stmt = $this->db->prepare("INSERT 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]);
@ -229,14 +256,14 @@ class RateLimiter {
$createdBy, $createdBy,
$description $description
); );
$this->log->insertLog($userId ?? 0, $logMessage, 'system'); $this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
} }
return $result; return $result;
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), 'system'); $this->logger->log('error', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => '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;
@ -264,14 +291,14 @@ class RateLimiter {
$removedBy, $removedBy,
$ipDetails['created_by'] $ipDetails['created_by']
); );
$this->log->insertLog($userId ?? 0, $logMessage, 'system'); $this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
} }
return $result; return $result;
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), 'system'); $this->logger->log('error', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => '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;
@ -284,7 +311,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->log->insertLog($userId, "IP Blacklist: {$message}", 'system'); $this->logger->log('info', "IP Blacklist: {$message}", ['user_id' => $userId, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', $message); Feedback::flash('ERROR', 'DEFAULT', $message);
} }
return false; return false;
@ -292,9 +319,14 @@ 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 OR REPLACE INTO {$this->blacklistTable} $stmt = $this->db->prepare("INSERT 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]);
@ -307,13 +339,13 @@ class RateLimiter {
$reason, $reason,
$expiryTime ?? 'never' $expiryTime ?? 'never'
); );
$this->log->insertLog($userId ?? 0, $logMessage, 'system'); $this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
} }
return $result; return $result;
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), 'system'); $this->logger->log('error', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => '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;
@ -341,13 +373,13 @@ class RateLimiter {
$ipDetails['created_by'], $ipDetails['created_by'],
$ipDetails['reason'] $ipDetails['reason']
); );
$this->log->insertLog($userId ?? 0, $logMessage, 'system'); $this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
} }
return $result; return $result;
} catch (Exception $e) { } catch (Exception $e) {
if ($userId) { if ($userId) {
$this->log->insertLog($userId, "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), 'system'); $this->logger->log('error', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => '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;
@ -372,17 +404,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 < datetime('now')"); WHERE expiry_time IS NOT NULL AND expiry_time < 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 < datetime('now', '-' || :minutes || ' minutes')"); WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)");
$stmt->execute([':minutes' => $this->decayMinutes]); $stmt->execute([':minutes' => $this->decayMinutes]);
return true; return true;
} catch (Exception $e) { } catch (Exception $e) {
$this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system'); $this->logger->log('error', "Failed to cleanup expired entries: " . $e->getMessage(), ['user_id' => $userId ?? null, 'scope' => '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;
} }
@ -411,7 +443,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 > datetime('now', '-' || :minutes || ' minutes')"; AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
':ip' => $ipAddress, ':ip' => $ipAddress,
@ -423,7 +455,12 @@ class RateLimiter {
return $result['total_attempts'] < $this->autoBlacklistThreshold; return $result['total_attempts'] < $this->autoBlacklistThreshold;
} }
public function attempt($username, $ipAddress) { public function attempt($username, $ipAddress, $failed = true) {
// 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);
@ -444,7 +481,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 > datetime('now', '-' || :minutes || ' minutes')"; AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -480,7 +517,7 @@ class RateLimiter {
public function clearOldAttempts() { public function clearOldAttempts() {
$sql = "DELETE FROM {$this->authRatelimitTable} $sql = "DELETE FROM {$this->authRatelimitTable}
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')"; WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -493,7 +530,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 > datetime('now', '-' || :minutes || ' minutes')"; AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -535,7 +572,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 >= DATETIME('now', '-1 minute')"; AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -566,7 +603,7 @@ class RateLimiter {
*/ */
private function cleanOldPageRequests() { private function cleanOldPageRequests() {
$sql = "DELETE FROM {$this->pagesRatelimitTable} $sql = "DELETE FROM {$this->pagesRatelimitTable}
WHERE request_time < DATETIME('now', '-1 minute')"; WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute(); $stmt->execute();
@ -578,8 +615,10 @@ 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) {
$userObj = new User($this->db); // Check admin rights directly from database
if ($userObj->hasRight($userId, 'admin')) { $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 = ?');
$stmt->execute([$userId, 'superuser']);
if ($stmt->fetchColumn() > 0) {
return $this->pageLimits['admin']; return $this->pageLimits['admin'];
} }
} }
@ -615,7 +654,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 > DATETIME('now', '-1 minute')"; AND request_time > DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([

View File

@ -0,0 +1,265 @@
<?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 registration, login, rights management, and profile updates. * Handles user-related functionalities such as login, rights management, and profile updates.
*/ */
class User { class User {
/** /**
@ -33,63 +33,6 @@ 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.
* *
@ -101,12 +44,8 @@ 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();
@ -114,7 +53,7 @@ class User {
} }
// Then check credentials // Then check credentials
$query = $this->db->prepare("SELECT * FROM users WHERE username = :username"); $query = $this->db->prepare("SELECT * FROM user WHERE username = :username");
$query->bindParam(':username', $username); $query->bindParam(':username', $username);
$query->execute(); $query->execute();
@ -153,7 +92,10 @@ 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);
throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining."); return [
'status' => 'failed',
'message' => "Invalid credentials. {$remainingAttempts} attempts remaining."
];
} }
@ -166,7 +108,7 @@ class User {
*/ */
// FIXME not used now? // FIXME not used now?
public function getUserId($username) { public function getUserId($username) {
$sql = 'SELECT id FROM users WHERE username = :username'; $sql = 'SELECT id FROM user WHERE username = :username';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->bindParam(':username', $username); $query->bindParam(':username', $username);
@ -180,24 +122,24 @@ class User {
/** /**
* Fetches user details by user ID. * Fetches user details by user ID.
* *
* @param int $user_id The user ID. * @param int $userId 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($user_id) { public function getUserDetails($userId) {
$sql = 'SELECT $sql = 'SELECT
um.*, um.*,
u.username u.username
FROM FROM
users_meta um user_meta um
LEFT JOIN users u LEFT JOIN user 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' => $user_id, ':user_id' => $userId,
]); ]);
return $query->fetchAll(PDO::FETCH_ASSOC); return $query->fetchAll(PDO::FETCH_ASSOC);
@ -208,19 +150,19 @@ class User {
/** /**
* Grants a user a specific right. * Grants a user a specific right.
* *
* @param int $user_id The user ID. * @param int $userId 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($user_id, $right_id) { public function addUserRight($userId, $right_id) {
$sql = 'INSERT INTO users_rights $sql = 'INSERT INTO user_right
(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' => $user_id, ':user_id' => $userId,
':right_id' => $right_id, ':right_id' => $right_id,
]); ]);
} }
@ -229,20 +171,20 @@ class User {
/** /**
* Revokes a specific right from a user. * Revokes a specific right from a user.
* *
* @param int $user_id The user ID. * @param int $userId 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($user_id, $right_id) { public function removeUserRight($userId, $right_id) {
$sql = 'DELETE FROM users_rights $sql = 'DELETE FROM user_right
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' => $user_id, ':user_id' => $userId,
':right_id' => $right_id, ':right_id' => $right_id,
]); ]);
} }
@ -257,7 +199,7 @@ class User {
$sql = 'SELECT $sql = 'SELECT
id AS right_id, id AS right_id,
name AS right_name name AS right_name
FROM rights FROM `right`
ORDER BY id ASC'; ORDER BY id ASC';
$query = $this->db->prepare($sql); $query = $this->db->prepare($sql);
$query->execute(); $query->execute();
@ -270,27 +212,27 @@ class User {
/** /**
* Retrieves the rights assigned to a specific user. * Retrieves the rights assigned to a specific user.
* *
* @param int $user_id The user ID. * @param int $userId The user ID.
* *
* @return array List of user rights. * @return array List of user rights.
*/ */
public function getUserRights($user_id) { public function getUserRights($userId) {
$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
users u `user` u
LEFT JOIN users_rights ur LEFT JOIN `user_right` ur
ON u.id = ur.user_id ON u.id = ur.user_id
LEFT JOIN rights r LEFT JOIN `right` 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' => $user_id, ':user_id' => $userId,
]); ]);
$result = $query->fetchAll(PDO::FETCH_ASSOC); $result = $query->fetchAll(PDO::FETCH_ASSOC);
@ -299,7 +241,7 @@ class User {
$specialEntries = []; $specialEntries = [];
// user 1 is always superuser // user 1 is always superuser
if ($user_id == 1) { if ($userId == 1) {
$specialEntries = [ $specialEntries = [
[ [
'user_id' => 1, 'user_id' => 1,
@ -309,7 +251,7 @@ class User {
]; ];
// user 2 is always demo // user 2 is always demo
} elseif ($user_id == 2) { } elseif ($userId == 2) {
$specialEntries = [ $specialEntries = [
[ [
'user_id' => 2, 'user_id' => 2,
@ -333,17 +275,17 @@ class User {
/** /**
* Check if the user has a specific right. * Check if the user has a specific right.
* *
* @param int $user_id The user ID. * @param int $userId 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($user_id, $right_name) { function hasRight($userId, $right_name) {
$userRights = $this->getUserRights($user_id); $userRights = $this->getUserRights($userId);
$userHasRight = false; $userHasRight = false;
// superuser always has all the rights // superuser always has all the rights
if ($user_id === 1) { if ($userId === 1) {
$userHasRight = true; $userHasRight = true;
} }
@ -362,8 +304,8 @@ class User {
/** /**
* Updates a user's metadata in the database. * Updates a user's metadata in the database.
* *
* @param int $user_id The ID of the user to update. * @param int $userId 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.
@ -371,9 +313,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($user_id, $updatedUser) { public function editUser($userId, $updatedUser) {
try { try {
$sql = 'UPDATE users_meta SET $sql = 'UPDATE user_meta SET
name = :name, name = :name,
email = :email, email = :email,
timezone = :timezone, timezone = :timezone,
@ -381,7 +323,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' => $user_id, ':user_id' => $userId,
':name' => $updatedUser['name'], ':name' => $updatedUser['name'],
':email' => $updatedUser['email'], ':email' => $updatedUser['email'],
':timezone' => $updatedUser['timezone'], ':timezone' => $updatedUser['timezone'],
@ -400,20 +342,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 $user_id The ID of the user whose avatar is being removed. * @param int $userId 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($user_id, $old_avatar = '') { public function removeAvatar($userId, $old_avatar = '') {
try { try {
// remove from database // remove from database
$sql = 'UPDATE users_meta SET $sql = 'UPDATE user_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' => $user_id, ':user_id' => $userId,
]); ]);
// delete the old avatar file // delete the old avatar file
@ -433,14 +375,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 $user_id The ID of the user whose avatar is being updated. * @param int $userId 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($user_id, $avatar_file, $avatars_path) { public function changeAvatar($userId, $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) {
@ -457,13 +399,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 users_meta SET $sql = 'UPDATE user_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' => $user_id ':user_id' => $userId
]); ]);
// all went OK // all went OK
$_SESSION['notice'] .= 'Avatar updated successfully. '; $_SESSION['notice'] .= 'Avatar updated successfully. ';
@ -493,7 +435,7 @@ class User {
*/ */
public function getUsers() { public function getUsers() {
$sql = "SELECT id, username $sql = "SELECT id, username
FROM users FROM `user`
ORDER BY username ASC"; ORDER BY username ASC";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
@ -505,9 +447,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) {
@ -527,8 +469,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) {
@ -548,15 +490,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 users WHERE id = :user_id"; $sql = "SELECT password FROM user 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);
@ -569,7 +511,7 @@ class User {
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT); $hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
// Update the password // Update the password
$sql = "UPDATE users SET password = :password WHERE id = :user_id"; $sql = "UPDATE user 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,8 +10,17 @@ 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 inteerface // site name used in emails and in the interface
'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
@ -22,12 +31,20 @@ return [
//******************************************* //*******************************************
// database // database
'db' => [ 'db_type' => 'mariadb',
// DB type for the web app, currently only "sqlite" is used
'db_type' => 'sqlite', '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

@ -0,0 +1,35 @@
<?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

@ -0,0 +1,41 @@
<?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

@ -0,0 +1,37 @@
<?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

@ -0,0 +1,53 @@
<?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

@ -0,0 +1,30 @@
<?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

@ -0,0 +1,17 @@
<?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

@ -0,0 +1,37 @@
<?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

@ -0,0 +1,57 @@
<?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

@ -0,0 +1,24 @@
<?php
/**
* Returns the user's IP address.
* Uses global $user_IP set by Logger plugin if available, else falls back to server variables.
*
* @return string
*/
function getUserIP() {
global $user_IP;
if (!empty($user_IP)) {
return $user_IP;
}
// Fallback to HTTP headers or REMOTE_ADDR
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
// May contain multiple IPs
$parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
return trim($parts[0]);
}
return $_SERVER['REMOTE_ADDR'] ?? '';
}

View File

@ -0,0 +1,15 @@
<?php
/**
* Returns a logger instance: plugin Log if available, otherwise NullLogger.
*
* @param mixed $database Database or DatabaseConnector instance.
* @return mixed Logger instance with 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

@ -1,20 +0,0 @@
<?php
function getUserIP() {
// get directly the user IP
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
// if user is behind some proxy
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
// get only the first IP if there are more
if (strpos($ip, ',') !== false) {
$ip = explode(',', $ip)[0];
}
return trim($ip);
}

View File

@ -0,0 +1,109 @@
<?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

@ -0,0 +1,307 @@
<?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

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

View File

@ -1,10 +1,9 @@
<?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; global $logObject, $user_IP;
$security = SecurityHelper::getInstance(); $security = SecurityHelper::getInstance();
// Skip CSRF check for GET requests // Skip CSRF check for GET requests
@ -34,14 +33,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 = getUserIP(); $ipAddress = $user_IP;
$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->insertLog(null, $logMessage, 'system'); $logObject->log('error', $logMessage, ['user_id' => null, 'scope' => 'system']);
// Return error message // Return error message
http_response_code(403); http_response_code(403);

View File

@ -1,61 +1,63 @@
<?php <?php
// connect to database // connect to database
function connectDB($config, $database = '', $dbFile = '', $platformId = '') { function connectDB($config) {
// sqlite database file
// connecting ti a jilo sqlite database if ($config['db_type'] === 'sqlite') {
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("Invalid platform ID \"{$platformId}\", database file \"{$dbFile}\" not found.")); throw new Exception(getError("Database file \"{$dbFile}\"not found."));
} }
$db = new Database([ $db = new Database([
'type' => 'sqlite', 'type' => $config['db_type'],
'dbFile' => $dbFile, 'dbFile' => $dbFile,
]); ]);
return ['db' => $db, 'error' => null]; $pdo = $db->getConnection();
} catch (Exception $e) { } catch (Exception $e) {
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())]; Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to DB.', $e->getMessage()));
return false;
} }
return $db;
// connecting to a jilo-web database of the web app // mysql/mariadb database
} 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']}\""));
// sqlite database file return false;
if ($config['db']['db_type'] === 'sqlite') {
try {
$db = new Database([
'type' => $config['db']['db_type'],
'dbFile' => $config['db']['sqlite_file'],
]);
$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();
}
} }
} }
// connect to Jilo database
function connectJiloDB($config, $dbFile = '', $platformId = '') {
try {
if (!$dbFile || !file_exists($dbFile)) {
throw new Exception(getError("Invalid platform ID \"{$platformId}\", database file \"{$dbFile}\" not found."));
}
$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,7 +1,6 @@
<?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
@ -13,10 +12,10 @@ require_once __DIR__ . '/../helpers/logs.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; global $app_root, $user_IP;
$isTest = defined('PHPUNIT_RUNNING'); $isTest = defined('PHPUNIT_RUNNING');
$rateLimiter = $existingRateLimiter ?? new RateLimiter($database); $rateLimiter = $existingRateLimiter ?? new RateLimiter($database);
$ipAddress = getUserIP(); $ipAddress = $user_IP;
// Check if request is allowed // Check if request is allowed
if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) { if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) {

View File

@ -1,97 +0,0 @@
<?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,10 +11,16 @@ 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.',
@ -30,10 +36,9 @@ 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.',
], ],
'REGISTER' => [ 'THEME' => [
'SUCCESS' => 'Registration successful. You can log in now.', 'THEME_CHANGE_SUCCESS' => 'Theme has been changed successfully.',
'FAILED' => 'Registration failed: %s', 'THEME_CHANGE_FAILED' => 'Failed to change theme. The selected theme may not be available.',
'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($dbWeb); $agentObject = new Agent($db);
$hostObject = new Host($dbWeb); $hostObject = new Host($db);
/** /**
* 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($dbWeb, 'contact', $user_id); checkRateLimit($db, 'contact', $userId);
// 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 = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); $response = connectJiloDB($config, $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 = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); $response = connectJiloDB($config, $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,8 +13,7 @@ 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($dbWeb); $userObject = new User($db);
$logObject = new Log($dbWeb);
$configObject = new Config(); $configObject = new Config();
// For AJAX requests // For AJAX requests
@ -51,8 +50,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($user_id, 'edit config file')) { if (!$userObject->hasRight($userId, 'edit config file')) {
$logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", 'system'); $logObject->log('error', "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", ['user_id' => $userId, 'scope' => '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;
@ -64,7 +63,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($dbWeb, 'config', $user_id); checkRateLimit($db, 'config', $userId);
// Ensure no output before this point // Ensure no output before this point
ob_clean(); ob_clean();
@ -74,7 +73,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->insertLog($user_id, "Failed to read request data for config update", 'system'); $logObject->log('error', "Failed to read request data for config update", ['user_id' => $userId, 'scope' => 'system']);
ApiResponse::error('Failed to read request data'); ApiResponse::error('Failed to read request data');
exit; exit;
} }
@ -115,10 +114,11 @@ if (!$isAjax) {
* Handles GET requests to display templates. * Handles GET requests to display templates.
*/ */
if ($userObject->hasRight($user_id, 'view config file')) { if ($userObject->hasRight($userId, 'superuser') ||
$userObject->hasRight($userId, 'view config file')) {
include '../app/templates/config.php'; include '../app/templates/config.php';
} else { } else {
$logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", 'system'); $logObject->log('error', "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", ['user_id' => $userId, 'scope' => 'system']);
include '../app/templates/error-unauthorized.php'; include '../app/templates/error-unauthorized.php';
} }
} }

View File

@ -14,17 +14,10 @@
* - `password`: Change password * - `password`: Change password
*/ */
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header("Location: $app_root?page=login");
exit();
}
$user_id = $_SESSION['user_id'];
// Initialize user object // Initialize user object
$userObject = new User($dbWeb); $userObject = new User($db);
// Get action and item from request
$action = $_REQUEST['action'] ?? ''; $action = $_REQUEST['action'] ?? '';
$item = $_REQUEST['item'] ?? ''; $item = $_REQUEST['item'] ?? '';
@ -40,7 +33,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($dbWeb, 'credentials', $user_id); checkRateLimit($db, 'credentials', $userId);
switch ($item) { switch ($item) {
case '2fa': case '2fa':
@ -50,7 +43,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$code = $_POST['code'] ?? ''; $code = $_POST['code'] ?? '';
$secret = $_POST['secret'] ?? ''; $secret = $_POST['secret'] ?? '';
if ($userObject->enableTwoFactor($user_id, $secret, $code)) { if ($userObject->enableTwoFactor($userId, $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();
@ -67,7 +60,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($user_id, $code)) { if ($userObject->verifyTwoFactor($userId, $code)) {
$_SESSION['2fa_verified'] = true; $_SESSION['2fa_verified'] = true;
header("Location: $app_root?page=dashboard"); header("Location: $app_root?page=dashboard");
exit(); exit();
@ -79,7 +72,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
break; break;
case 'disable': case 'disable':
if ($userObject->disableTwoFactor($user_id)) { if ($userObject->disableTwoFactor($userId)) {
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.');
@ -96,8 +89,7 @@ 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,
@ -115,7 +107,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
exit(); exit();
} }
if ($userObject->changePassword($user_id, $_POST['current_password'], $_POST['new_password'])) { if ($userObject->changePassword($userId, $_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.');
@ -136,12 +128,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($user_id); $has2fa = $userObject->isTwoFactorEnabled($userId);
switch ($action) { switch ($action) {
case 'setup': case 'setup':
if (!$has2fa) { if (!$has2fa) {
$result = $userObject->enableTwoFactor($user_id); $result = $userObject->enableTwoFactor($userId);
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 = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); $response = connectJiloDB($config, $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($dbWeb); $agentObject = new Agent($db);
$hostObject = new Host($dbWeb); $hostObject = new Host($db);
// Connect to Jilo database for log data // Connect to Jilo database for log data
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); $response = connectJiloDB($config, $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($dbWeb); $agentObject = new Agent($db);
$hostObject = new Host($dbWeb); $hostObject = new Host($db);
// 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']; $db = connectDB($config);
// 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,21 +33,23 @@ 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'] ?? '';
$userId = $_SESSION['2fa_pending_user_id']; $pending2FA = Session::get2FAPending();
$username = $_SESSION['2fa_pending_username'];
$rememberMe = isset($_SESSION['2fa_pending_remember']); if (!$pending2FA) {
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($userId, $code)) { if ($twoFactorAuth->verify($pending2FA['user_id'], $code)) {
// Complete login // Complete login
handleSuccessfulLogin($userId, $username, $rememberMe, $config, $logObject, $user_IP); handleSuccessfulLogin($pending2FA['user_id'], $pending2FA['username'],
$pending2FA['remember_me'], $config, $app_root, $logObject, $user_IP);
// Clean up 2FA session data // Clean up 2FA session data
unset($_SESSION['2fa_pending_user_id']); Session::clear2FAPending();
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
exit(); exit();
} }
@ -60,6 +62,9 @@ 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();
@ -92,7 +97,7 @@ try {
// Process reset request // Process reset request
require_once '../app/classes/passwordReset.php'; require_once '../app/classes/passwordReset.php';
$resetHandler = new PasswordReset($db); $resetHandler = new PasswordReset($db, $config);
$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
@ -118,7 +123,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); $resetHandler = new PasswordReset($db, $config);
$token = $_GET['token']; $token = $_GET['token'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@ -196,8 +201,7 @@ try {
], ],
'password' => [ 'password' => [
'type' => 'string', 'type' => 'string',
'required' => true, 'required' => true
'min' => 5
] ]
]; ];
@ -220,9 +224,6 @@ 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
@ -232,11 +233,8 @@ try {
switch ($loginResult['status']) { switch ($loginResult['status']) {
case 'requires_2fa': case 'requires_2fa':
// Store pending 2FA info // Store pending 2FA info
$_SESSION['2fa_pending_user_id'] = $loginResult['user_id']; Session::store2FAPending($loginResult['user_id'], $loginResult['username'],
$_SESSION['2fa_pending_username'] = $loginResult['username']; isset($formData['remember_me']));
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');
@ -245,7 +243,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, $logObject, $user_IP); isset($formData['remember_me']), $config, $app_root, $logObject, $user_IP);
exit(); exit();
default: default:
@ -258,8 +256,9 @@ 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)) {
$user_id = $userObject->getUserId($username)[0]['id'] ?? 0; $userId = $userObject->getUserId($username)[0]['id'] ?? 0;
$logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user'); $logObject->log('error', "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", ['user_id' => $userId, 'scope' => 'user']);
$rateLimiter->attempt($username, $user_IP);
} }
} }
} }
@ -269,7 +268,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); echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false);
} }
// Get any new feedback messages // Get any new feedback messages
@ -281,42 +280,26 @@ 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, $logObject, $userIP) { function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $app_root, $logObject, $userIP) {
if ($rememberMe) { // Create authenticated session
// 30*24*60*60 = 30 days Session::createAuthSession($userId, $username, $rememberMe, $config);
$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->insertLog($userId, "Login: User \"$username\" logged in. IP: $userIP", 'user'); $logObject->log('info', "Login: User \"$username\" logged in. IP: $userIP", ['user_id' => $userId, 'scope' => 'user']);
// Set success message and redirect // Set success message
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

@ -9,7 +9,7 @@
*/ */
// connect to database // connect to database
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); $response = connectJiloDB($config, $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($dbWeb, 'profile', $user_id); checkRateLimit($db, 'profile', $userId);
// avatar removal // avatar removal
if ($item === 'avatar' && $action === 'remove') { if ($item === 'avatar' && $action === 'remove') {
$validator = new Validator(['user_id' => $user_id]); $validator = new Validator(['user_id' => $userId]);
$rules = [ $rules = [
'user_id' => [ 'user_id' => [
'required' => true, 'required' => true,
@ -48,7 +48,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
exit(); exit();
} }
$result = $userObject->removeAvatar($user_id, $config['avatars_path'].$userDetails[0]['avatar']); $result = $userObject->removeAvatar($userId, $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,50 +89,54 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
'timezone' => htmlspecialchars($_POST['timezone'] ?? ''), 'timezone' => htmlspecialchars($_POST['timezone'] ?? ''),
'bio' => htmlspecialchars($_POST['bio'] ?? ''), 'bio' => htmlspecialchars($_POST['bio'] ?? ''),
]; ];
$result = $userObject->editUser($user_id, $updatedUser); $result = $userObject->editUser($userId, $updatedUser);
if ($result === true) { if ($result === true) {
Feedback::flash('NOTICE', 'DEFAULT', "User details for \"{$updatedUser['name']}\" are edited."); Feedback::flash('NOTICE', 'DEFAULT', "User details for \"{$userDetails[0]['username']}\" 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
if (isset($_POST['rights'])) { // Get current rights IDs
$validator = new Validator(['rights' => $_POST['rights']]); $userRightsIds = array_column($userRights, 'right_id');
$rules = [
'rights' => [
'array' => true
]
];
if (!$validator->validate($rules)) { // If no rights are selected, remove all rights
Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError()); if (!isset($_POST['rights'])) {
header("Location: $app_root?page=profile"); $_POST['rights'] = [];
exit(); }
}
$newRights = $_POST['rights']; $validator = new Validator(['rights' => $_POST['rights']]);
// extract the new right_ids $rules = [
$userRightsIds = array_column($userRights, 'right_id'); 'rights' => [
// what rights we need to add 'array' => true
$rightsToAdd = array_diff($newRights, $userRightsIds); ]
if (!empty($rightsToAdd)) { ];
foreach ($rightsToAdd as $rightId) {
$userObject->addUserRight($user_id, $rightId); 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 }
$rightsToRemove = array_diff($userRightsIds, $newRights); // what rights we need to remove
if (!empty($rightsToRemove)) { $rightsToRemove = array_diff($userRightsIds, $newRights);
foreach ($rightsToRemove as $rightId) { if (!empty($rightsToRemove)) {
$userObject->removeUserRight($user_id, $rightId); foreach ($rightsToRemove as $rightId) {
} $userObject->removeUserRight($userId, $rightId);
} }
} }
// update the avatar // update the avatar
if (!empty($_FILES['avatar_file']['tmp_name'])) { if (!empty($_FILES['avatar_file']['tmp_name'])) {
$result = $userObject->changeAvatar($user_id, $_FILES['avatar_file'], $config['avatars_path']); $result = $userObject->changeAvatar($userId, $_FILES['avatar_file'], $config['avatars_path']);
} }
header("Location: $app_root?page=profile"); header("Location: $app_root?page=profile");

View File

@ -1,15 +1,10 @@
<?php <?php
// Check if user has any of the required rights // Check if user has any of the required rights
if (!($userObject->hasRight($user_id, 'superuser') || if (!($userObject->hasRight($userId, 'superuser') ||
$userObject->hasRight($user_id, 'edit whitelist') || $userObject->hasRight($userId, 'edit whitelist') ||
$userObject->hasRight($user_id, 'edit blacklist') || $userObject->hasRight($userId, 'edit blacklist') ||
$userObject->hasRight($user_id, 'edit ratelimiting'))) { $userObject->hasRight($userId, '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;
} }
@ -19,7 +14,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($dbWeb); $rateLimiter = new RateLimiter($db);
// Handle form submissions // Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
@ -27,7 +22,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($dbWeb, 'security', $user_id); checkRateLimit($db, 'security', $userId);
$action = $_POST['action']; $action = $_POST['action'];
$validator = new Validator($_POST); $validator = new Validator($_POST);
@ -35,7 +30,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
try { try {
switch ($action) { switch ($action) {
case 'add_whitelist': case 'add_whitelist':
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) { if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -54,7 +49,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, $user_id)) { if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $userId)) {
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');
@ -65,7 +60,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
break; break;
case 'remove_whitelist': case 'remove_whitelist':
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) { if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -79,7 +74,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
]; ];
if ($validator->validate($rules)) { if ($validator->validate($rules)) {
if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $user_id)) { if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $userId)) {
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');
@ -90,7 +85,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
break; break;
case 'add_blacklist': case 'add_blacklist':
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) { if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -116,7 +111,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, $user_id, $expiry_hours)) { if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'], $currentUser, $userId, $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');
@ -127,7 +122,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
break; break;
case 'remove_blacklist': case 'remove_blacklist':
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) { if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) {
Feedback::flash('SECURITY', 'PERMISSION_DENIED'); Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break; break;
} }
@ -141,7 +136,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
]; ];
if ($validator->validate($rules)) { if ($validator->validate($rules)) {
if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $user_id)) { if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $userId)) {
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($dbWeb); $hostObject = new Host($db);
$agentObject = new Agent($dbWeb); $agentObject = new Agent($db);
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($dbWeb, 'profile', $user_id); checkRateLimit($db, 'profile', $userId);
// 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($user_id, 'view settings')) { if ($userObject->hasRight($userId, 'view settings') || $userObject->hasRight($userId, 'superuser')) {
$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($dbWeb); $agentObject = new Agent($db);
$hostObject = new Host($dbWeb); $hostObject = new Host($db);
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 = connectDB($config, 'jilo', $platform['jilo_database'], $platform['id']); $response = connectJiloDB($config, $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

@ -0,0 +1,10 @@
<?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

@ -0,0 +1,71 @@
<?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,7 +17,8 @@
<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($user_id, 'edit config file')) { ?> <?php if ($userObject->hasRight($userId, 'superuser') ||
$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
@ -37,7 +38,7 @@
<div class="card-body p-4"> <div class="card-body p-4">
<form id="configForm"> <form id="configForm">
<?php <?php
include 'csrf_token.php'; include CSRF_TOKEN_INCLUDE;
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.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<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 autofocus /> required />
</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=".{5,}" 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-group mb-3">
@ -21,6 +21,9 @@
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.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<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.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<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

@ -1,109 +0,0 @@
<!-- log events -->
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-md-6">
<h2 class="mb-0">Log events</h2>
<small>events recorded in the Jilo monitoring platform</small>
</div>
</div>
<!-- Tabs navigation -->
<ul class="nav nav-tabs mb-4">
<li class="nav-item">
<a class="nav-link <?= $scope === 'user' ? 'active' : '' ?>" href="?page=logs&tab=user">
Logs for current user
</a>
</li>
<?php if ($has_system_access) { ?>
<li class="nav-item">
<a class="nav-link <?= $scope === 'system' ? 'active' : '' ?>" href="?page=logs&tab=system">
Logs for all users
</a>
</li>
<?php } ?>
</ul>
<!-- logs filter -->
<div class="card mb-3">
<div class="card-body">
<form method="GET" action="" class="row g-3 align-items-end">
<input type="hidden" name="page" value="logs">
<input type="hidden" name="tab" value="<?= htmlspecialchars($scope) ?>">
<div class="col-md-3">
<label for="from_time" class="form-label">From date</label>
<input type="date" class="form-control" id="from_time" name="from_time" value="<?= htmlspecialchars($_REQUEST['from_time'] ?? '') ?>">
</div>
<div class="col-md-3">
<label for="until_time" class="form-label">Until date</label>
<input type="date" class="form-control" id="until_time" name="until_time" value="<?= htmlspecialchars($_REQUEST['until_time'] ?? '') ?>">
</div>
<?php if ($scope === 'system') { ?>
<div class="col-md-2">
<label for="id" class="form-label">User ID</label>
<input type="text" class="form-control" id="id" name="id" value="<?= htmlspecialchars($_REQUEST['id'] ?? '') ?>" placeholder="Enter user ID">
</div>
<?php } ?>
<div class="col-md">
<label for="message" class="form-label">Message</label>
<input type="text" class="form-control" id="message" name="message" value="<?= htmlspecialchars($_REQUEST['message'] ?? '') ?>" placeholder="Search in log messages">
</div>
<div class="col-md-auto">
<button type="submit" class="btn btn-primary me-2">
<i class="fas fa-search me-2"></i>Search
</button>
<a href="?page=logs&tab=<?= htmlspecialchars($scope) ?>" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Clear
</a>
</div>
</form>
</div>
</div>
<!-- /logs filter -->
<!-- logs -->
<?php if ($time_range_specified) { ?>
<div class="alert alert-info m-3">
<i class="fas fa-calendar-alt me-2"></i>Time period: <strong><?= htmlspecialchars($from_time) ?> - <?= htmlspecialchars($until_time) ?></strong>
</div>
<?php } ?>
<div class="mb-5">
<?php if (!empty($logs['records'])) { ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<?php if ($scope === 'system') { ?>
<th>Username (id)</th>
<?php } ?>
<th>Time</th>
<th>Log message</th>
</tr>
</thead>
<tbody>
<?php foreach ($logs['records'] as $row) { ?>
<tr>
<?php if ($scope === 'system') { ?>
<td><strong><?= htmlspecialchars($row['username']) ?> (<?= htmlspecialchars($row['userID']) ?>)</strong></td>
<?php } ?>
<td><span class="text-muted"><?= date('d M Y H:i', strtotime($row['time'])) ?></span></td>
<td><?= htmlspecialchars($row['log message']) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php include '../app/templates/pagination.php'; ?>
<?php } else { ?>
<div class="alert alert-info m-3">
<i class="fas fa-info-circle me-2"></i>No log entries found for the specified criteria.
</div>
<?php } ?>
</div>
</div>
<!-- /log events -->

View File

@ -12,7 +12,7 @@
</div> </div>
<?php if (isset($currentUser) && $page !== 'logout') { ?> <?php if (Session::getUsername() && $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 (isset($currentUser)) { ?> <?php if (Session::getUsername()) { ?>
<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']) ?> version&nbsp;<?= htmlspecialchars($config['version'] ?? '1.0.0') ?>
</li> </li>
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?> <?php if (Session::isValidSession()) { ?>
<?php foreach ($platformsAll as $platform) { <?php foreach ($platformsAll as $platform) {
$platform_switch_url = switchPlatform($platform['id']); $platform_switch_url = switchPlatform($platform['id']);
@ -40,13 +40,17 @@
</ul> </ul>
<ul class="menu-right"> <ul class="menu-right">
<?php if (isset($_SESSION['username']) && isset($_SESSION['user_id'])) { ?> <?php if (Session::isValidSession()) { ?>
<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>
@ -59,10 +63,49 @@
</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>
<li><a href="<?= htmlspecialchars($app_root) ?>?page=register">register</a></li> <?php do_hook('main_public_menu', ['app_root' => $app_root, 'section' => 'main', 'position' => 100]); ?>
<?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,45 +72,11 @@ $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,11 +6,14 @@
* $totalPages - Total number of pages * $totalPages - Total number of pages
*/ */
// Ensure required variables are set // Validate required pagination variables
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,6 +6,7 @@
<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">
@ -132,6 +133,7 @@
<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($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist')) { ?> <?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, '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($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist')) { ?> <?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, '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($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting')) { ?> <?php if ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, '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($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist'))) { ?> <?php if ($section === 'whitelist' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, '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.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<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.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<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($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist'))) { ?> <?php if ($section === 'blacklist' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, '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.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<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.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<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($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting'))) { ?> <?php if ($section === 'ratelimit' && ($userObject->hasRight($userId, 'superuser') || $userObject->hasRight($userId, '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($user_id, 'delete platform')): ?> <?php if ($userObject->hasRight($userId, '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

@ -0,0 +1,44 @@
<?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

@ -0,0 +1,243 @@
-- 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,4 +1,3 @@
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,
@ -25,43 +24,95 @@ CREATE TABLE rights (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE name TEXT NOT NULL UNIQUE
); );
INSERT INTO rights VALUES(1,'superuser'); CREATE TABLE IF NOT EXISTS "jilo_agent_types" (
INSERT INTO rights VALUES(2,'edit users'); id INTEGER PRIMARY KEY AUTOINCREMENT,
INSERT INTO rights VALUES(3,'view settings'); description TEXT,
INSERT INTO rights VALUES(4,'edit settings'); endpoint TEXT
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, user_id INTEGER NOT NULL PRIMARY KEY,
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,
PRIMARY KEY (user_id, code), FOREIGN KEY (user_id) REFERENCES users(id)
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,
@ -70,85 +121,5 @@ 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 user(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(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,24 +5,38 @@ 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 platforms VALUES(1,'meet.lindeas.com','https://meet.lindeas.com','../jilo-meet.lindeas.db'); INSERT INTO rights VALUES(1,'superuser');
INSERT INTO platforms VALUES(2,'example.com','https://meet.example.com','../jilo.db'); INSERT INTO rights VALUES(2,'edit users');
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 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(1,'jvb','/jvb');
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(2,'jicofo','/jicofo');
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(3,'prosody','/prosody');
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(4,'nginx','/nginx');
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 jilo_agent_types VALUES(5,'jibri','/jibri');
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 jilo_agents VALUES(1,1,1,'https://meet.lindeas.com:8081','mysecretkey',5); INSERT INTO platforms VALUES(1,'example.com','https://meet.example.com','../../jilo/jilo.db');
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 hosts VALUES(1,'meet.lindeas.com',2,'main machine'); INSERT INTO ip_whitelist VALUES(1,'127.0.0.1',0,'localhost IPv4','2025-01-04 11:39:08','system');
INSERT INTO hosts VALUES(2,'meet.example.com',2,'test'); 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');
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

@ -0,0 +1,62 @@
# Logger plugin
## Overview
The Logger plugin provides a modular, pluggable logging system for the application.
It logs user and system events to a MySQL table named `log`.
## Installation
1. Copy the entire `logger` folder into your project's `plugins/` directory.
2. Ensure `"enabled": true` in `plugins/logger/plugin.json`.
3. On first initialization, the plugin will create the `log` table if it does not already exist.
## Database Schema
The plugin defines the following table (auto-created):
```sql
CREATE TABLE IF NOT EXISTS `log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`scope` SET('user','system') NOT NULL,
`message` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `log_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
```
## Hook API
Core must call:
```php
// After DB connect:
do_hook('logger.system_init', ['db' => $db]);
```
The plugin listens on `logger.system_init`, runs auto-migration, then sets:
```php
$GLOBALS['logObject']; // instance of Log
$GLOBALS['user_IP']; // current user IP
```
Then in the code use:
```php
$logObject->insertLog($userId, 'Your message', 'user');
$data = $logObject->readLog($userId, 'user', $offset, $limit, $filters);
```
## File Structure
```
plugins/logger/
├─ bootstrap.php # registers hook
├─ plugin.json # metadata & enabled flag
├─ README.md # this documentation
├─ models/
│ ├─ Log.php # main Log class
│ └─ LoggerFactory.php# migration + factory
├─ helpers/
│ └─ logs.php # user IP helper
└─ migrations/
└─ create_log_table.sql
```
## Uninstall / Disable
- Set `"enabled": false` in `plugin.json` or delete the `plugins/logger/` folder.
- Core code will default to `NullLogger` and no logs will be written.

View File

@ -0,0 +1,33 @@
<?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

@ -8,12 +8,17 @@
* It supports pagination and filtering. * It supports pagination and filtering.
*/ */
// Get any new feedback messages // Define plugin base path if not already defined
include '../app/helpers/feedback.php'; if (!defined('PLUGIN_LOGS_PATH')) {
define('PLUGIN_LOGS_PATH', dirname(__FILE__, 2) . '/');
}
require_once PLUGIN_LOGS_PATH . 'models/Log.php';
require_once PLUGIN_LOGS_PATH . 'models/LoggerFactory.php';
require_once dirname(__FILE__, 4) . '/app/classes/user.php';
// Check for rights; user or system // Check for rights; user or system
$has_system_access = ($userObject->hasRight($user_id, 'superuser') || $has_system_access = ($userObject->hasRight($userId, 'superuser') ||
$userObject->hasRight($user_id, 'view app logs')); $userObject->hasRight($userId, 'view app logs'));
// Get current page for pagination // Get current page for pagination
$currentPage = $_REQUEST['page_num'] ?? 1; $currentPage = $_REQUEST['page_num'] ?? 1;
@ -69,8 +74,8 @@ if (isset($_REQUEST['tab'])) {
} }
// prepare the result // prepare the result
$search = $logObject->readLog($user_id, $scope, $offset, $items_per_page, $filters); $search = $logObject->readLog($userId, $scope, $offset, $items_per_page, $filters);
$search_all = $logObject->readLog($user_id, $scope, 0, 0, $filters); $search_all = $logObject->readLog($userId, $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
@ -86,6 +91,7 @@ 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 {
@ -94,6 +100,7 @@ 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']
); );
} }
@ -103,7 +110,13 @@ if (!empty($search)) {
} }
} }
$username = $userObject->getUserDetails($user_id)[0]['username']; $username = $userObject->getUserDetails($userId)[0]['username'];
// Load the template // Get any new feedback messages
include '../app/templates/logs.php'; include dirname(__FILE__, 4) . '/app/helpers/feedback.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

@ -0,0 +1,15 @@
<?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

@ -0,0 +1,14 @@
-- --------------------------------------------------------
-- 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

@ -0,0 +1,119 @@
<?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

@ -0,0 +1,34 @@
<?php
/**
* LoggerFactory for Logger Plugin.
*
* Responsible for auto-migration and creating the Log instance.
*/
class LoggerFactory
{
/**
* @param object $db Database connector instance.
* @return array [Log $logger, string $userIP]
*/
public static function create($db): array
{
// Auto-migration: ensure log table exists
$pdo = $db->getConnection();
$migrationFile = __DIR__ . '/../migrations/create_log_table.sql';
if (file_exists($migrationFile)) {
$sql = file_get_contents($migrationFile);
$pdo->exec($sql);
}
// Load models and core IP helper
require_once __DIR__ . '/Log.php';
require_once __DIR__ . '/../../../app/helpers/ip_helper.php';
// Instantiate logger and retrieve user IP
$logger = new \Log($db);
$userIP = getUserIP();
return [$logger, $userIP];
}
}

View File

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

View File

@ -0,0 +1,111 @@
<!-- 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

@ -0,0 +1,24 @@
<?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

@ -8,24 +8,31 @@
* 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 $dbWeb, $logObject, $userObject; global $db, $logObject, $userObject;
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) { if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
// Apply rate limiting // Apply rate limiting
require '../app/includes/rate_limit_middleware.php'; require_once dirname(__FILE__, 4) . '/app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'register'); checkRateLimit($db, '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']); $formData = $security->sanitizeArray($_POST, ['username', 'password', 'confirm_password', 'csrf_token', 'terms']);
// Validate CSRF token // Validate CSRF token
if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) { if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) {
@ -47,6 +54,10 @@ if ($config['registration_enabled'] == true) {
'confirm_password' => [ 'confirm_password' => [
'required' => true, 'required' => true,
'matches' => 'password' 'matches' => 'password'
],
'terms' => [
'required' => true,
'equals' => 'on'
] ]
]; ];
@ -56,41 +67,42 @@ if ($config['registration_enabled'] == true) {
$password = $formData['password']; $password = $formData['password'];
// registering // registering
$result = $userObject->register($username, $password); $register = new Register($db);
$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
$user_id = $userObject->getUserId($username)[0]['id']; $userId = $userObject->getUserId($username)[0]['id'];
$logObject->insertLog($user_id, "Registration: New user \"$username\" registered successfully. IP: $user_IP", 'user'); $logObject->log('info', "Registration: New user \"$username\" registered successfully. IP: $user_IP", ['user_id' => $userId, 'scope' => '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)); header('Location: ' . htmlspecialchars($app_root . '?page=login'));
exit(); exit();
// registration fail, redirect to login // registration fail, redirect to login
} else { } else {
$logObject->insertLog(0, "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", 'system'); $logObject->log('error', "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", ['user_id' => null, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result"); Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result");
header('Location: ' . htmlspecialchars($app_root)); header('Location: ' . htmlspecialchars($app_root . '?page=register'));
exit(); exit();
} }
} else { } else {
$error = $validator->getFirstError(); $error = $validator->getFirstError();
$logObject->insertLog(0, "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", 'system'); $logObject->log('error', "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", ['user_id' => null, 'scope' => '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->insertLog(0, "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), 'system'); $logObject->log('error', "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), ['user_id' => null, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
} }
// Get any new feedback messages // Get any new feedback messages
include '../app/helpers/feedback.php'; include dirname(__FILE__, 4) . '/app/helpers/feedback.php';
// Load the template // Load the template
include '../app/templates/form-register.php'; include PLUGIN_REGISTER_PATH . 'views/form-register.php';
// registration disabled // registration disabled
} else { } else {

View File

@ -0,0 +1,92 @@
<?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

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

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.php'; ?> <?php include CSRF_TOKEN_INCLUDE; ?>
<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 autofocus /> required />
</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,6 +20,17 @@
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

@ -11,18 +11,126 @@
* 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();
// Apply security headers // Start session before any session-dependent code
require_once '../app/includes/security_headers_middleware.php'; require_once '../app/classes/session.php';
// sanitize all input vars that may end up in URLs or forms // Initialize themes system after session is started
require '../app/includes/sanitize.php'; require_once __DIR__ . '/../app/helpers/theme.php';
use app\Helpers\Theme;
session_name('jilo'); Session::startSession();
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';
@ -30,120 +138,81 @@ $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',
'conferences', 'graphs','latest','livejs','agents',
'participants', 'profile','credentials','config','security',
'components', 'settings','theme','theme-asset',
'graphs',
'latest',
'livejs',
'agents',
'config',
'profile',
'credentials',
'settings',
'security',
'status', 'status',
'logs', 'help','about',
'help', 'login','logout',
'login',
'logout',
'register',
]; ];
// cnfig file // Let plugins filter/extend allowed_urls
// possible locations, in order of preference $allowed_urls = filter_allowed_urls($allowed_urls);
$config_file_locations = [
__DIR__ . '/../app/config/jilo-web.conf.php', // Dispatch routing and auth
__DIR__ . '/../jilo-web.conf.php', require_once __DIR__ . '/../app/core/Router.php';
'/srv/jilo-web/jilo-web.conf.php', use App\Core\Router;
'/opt/jilo-web/jilo-web.conf.php' $currentUser = Router::checkAuth($config, $app_root, $public_pages, $page);
];
$config_file = null; // Connect to DB via DatabaseConnector
// try to find the config file require_once __DIR__ . '/../app/core/DatabaseConnector.php';
foreach ($config_file_locations as $location) { use App\Core\DatabaseConnector;
if (file_exists($location)) { $db = DatabaseConnector::connect($config);
$config_file = $location;
break; // Logging: default to NullLogger, plugin can override
} require_once __DIR__ . '/../app/core/NullLogger.php';
use App\Core\NullLogger;
$logObject = new NullLogger();
// Get the user IP
require_once __DIR__ . '/../app/helpers/ip_helper.php';
$user_IP = '';
// Plugin: initialize logging system plugin if available
do_hook('logger.system_init', ['db' => $db]);
// Override defaults if plugin provided real logger
if (isset($GLOBALS['logObject'])) {
$logObject = $GLOBALS['logObject'];
} }
// if found, use it if (isset($GLOBALS['user_IP'])) {
if ($config_file) { $user_IP = $GLOBALS['user_IP'];
$localConfigPath = str_replace(__DIR__ . '/..', '', $config_file);
$config = require $config_file;
} else {
die('Config file not found');
} }
$app_root = $config['folder']; // CSRF middleware and run pipeline
$pipeline->add(function() {
// check if logged in // Initialize security middleware
unset($currentUser); require_once __DIR__ . '/../app/includes/csrf_middleware.php';
if (isset($_COOKIE['username'])) { require_once __DIR__ . '/../app/helpers/security.php';
if ( !isset($_SESSION['username']) ) { $security = SecurityHelper::getInstance();
$_SESSION['username'] = $_COOKIE['username']; // Verify CSRF token for POST requests
} 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($dbWeb); $platformObject = new Platform($db);
$platformsAll = $platformObject->getPlatformDetails(); $platformsAll = $platformObject->getPlatformDetails();
// by default we connect ot the first configured platform // by default we connect ot the first configured platform
@ -153,54 +222,50 @@ 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') {
// get user info before destroying session // Save config before destroying session
$user_id = $userObject->getUserId($currentUser)[0]['id']; $savedConfig = $config;
// clean up session // clean up session
session_unset(); Session::destroySession();
session_destroy();
// start new session for the login page // start new session for the login page
session_start(); Session::startSession();
// 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->insertLog($user_id, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user'); $logObject->log('info', "Logout: User \"$currentUser\" logged out. IP: $user_IP", ['user_id' => $userId, 'scope' => 'user']);
// Set success message // Set success message
Feedback::flash('LOGIN', 'LOGOUT_SUCCESS'); Feedback::flash('LOGIN', 'LOGOUT_SUCCESS');
include '../app/templates/page-header.php'; // Use theme helper to include templates
include '../app/templates/page-menu.php'; \App\Helpers\Theme::include('page-header');
\App\Helpers\Theme::include('page-menu');
include '../app/pages/login.php'; include '../app/pages/login.php';
include '../app/templates/page-footer.php'; \App\Helpers\Theme::include('page-footer');
} 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 (isset($currentUser)) { if ($validSession) {
// 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();
} }
$user_id = $userObject->getUserId($currentUser)[0]['id']; $userDetails = $userObject->getUserDetails($userId);
$userDetails = $userObject->getUserDetails($user_id); $userRights = $userObject->getUserRights($userId);
$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($dbWeb); $serverObject = new Server($db);
$server_host = '127.0.0.1'; $server_host = '127.0.0.1';
$server_port = '8080'; $server_port = '8080';
@ -211,28 +276,70 @@ if ($page == 'logout') {
} }
} }
// List of pages that don't require authentication // --- Plugin loading logic for all enabled plugins ---
$public_pages = ['login', 'register']; // Ensure all enabled plugin bootstraps are loaded before mapping controllers
foreach ($GLOBALS['enabled_plugins'] as $plugin_name => $plugin_info) {
// Check if the requested page requires authentication $bootstrap_path = $plugin_info['path'] . '/bootstrap.php';
if (!in_array($page, $public_pages)) { if (file_exists($bootstrap_path)) {
require_once '../app/includes/session_middleware.php'; require_once $bootstrap_path;
}
}
// 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
include '../app/templates/page-header.php';
include '../app/templates/page-menu.php';
if (isset($currentUser)) {
include '../app/templates/page-sidebar.php';
}
if (in_array($page, $allowed_urls)) { if (in_array($page, $allowed_urls)) {
// all normal pages // The page is in allowed URLs
include "../app/pages/{$page}.php"; if (isset($mapped_plugin_controllers[$page]) && file_exists($mapped_plugin_controllers[$page])) {
// The page is from a plugin controller
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 { } else {
// the page is not in allowed urls, loading "not found" page // 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'; include '../app/templates/error-notfound.php';
\App\Helpers\Theme::include('page-footer');
} }
include '../app/templates/page-footer.php';
} }
// flush the output buffer and show the page // flush the output buffer and show the page

View File

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

View File

@ -0,0 +1,19 @@
<?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,137 +16,235 @@ 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' => 'sqlite', 'type' => 'mariadb',
'dbFile' => ':memory:' 'host' => $host,
'port' => '3306',
'dbname' => 'jilo_test',
'user' => 'test_jilo',
'password' => $password
]); ]);
// Create rate limiter table // Create rate limiter instance
$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);
// Mock $_SERVER variables // Drop tables if they exist
$_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth");
$_SERVER['REQUEST_URI'] = '/login'; $this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_page");
$_SERVER['REQUEST_METHOD'] = 'POST'; $this->db->getConnection()->exec("DROP TABLE IF EXISTS security_ip_blacklist");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_ip_whitelist");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS log");
// Define testing constant // Create required tables with correct names from RateLimiter class
if (!defined('TESTING')) { $this->db->getConnection()->exec("
define('TESTING', true); 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 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 rate limit records // Clean up all rate limit records
$this->db->getConnection()->exec('DELETE FROM pages_rate_limits'); $this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
$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()
{ {
// Test multiple requests // Clean any existing rate limit records
for ($i = 1; $i <= 5; $i++) { $this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
$result = checkRateLimit($this->db, '/login');
if ($i <= 5) { // Make 60 requests to reach the limit
// First 5 requests should pass for ($i = 0; $i < 60; $i++) {
$this->assertTrue($result); $result = checkRateLimit($this->db, '/login');
} else { $this->assertTrue($result, "Request $i should be allowed");
// 6th and subsequent requests should be blocked
$this->assertFalse($result); // 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
$result = checkRateLimit($this->db, '/login');
$this->assertFalse($result, "Request should be blocked after 60 requests");
} }
public function testRateLimitBypass() public function testRateLimitBypass()
{ {
// Test AJAX request bypass // Clean any existing rate limit records and lists
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'; $this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
$result = checkRateLimit($this->db, '/login'); $this->db->getConnection()->exec("TRUNCATE TABLE security_ip_whitelist");
$this->assertTrue($result); $this->db->getConnection()->exec("TRUNCATE TABLE security_ip_blacklist");
// 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()
{ {
// Use up the rate limit // Clean any existing rate limit records
for ($i = 0; $i < 5; $i++) { $this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
checkRateLimit($this->db, '/login');
// Make some requests
for ($i = 0; $i < 15; $i++) {
$result = checkRateLimit($this->db, '/login');
} }
// Wait for rate limit to reset (use a short window for testing) // Manually expire old requests
sleep(2); $this->db->getConnection()->exec("
DELETE FROM security_rate_page
WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)
");
// Should be able to make request again // Should be able to make requests again
$result = checkRateLimit($this->db, '/login'); $result = checkRateLimit($this->db, '/login');
$this->assertTrue($result); $this->assertTrue($result);
} }
public function testDifferentEndpoints() public function testDifferentEndpoints()
{ {
// Use up rate limit for login // Clean any existing rate limit records
for ($i = 0; $i < 5; $i++) { $this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
checkRateLimit($this->db, '/login');
// 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");
} }
// Should still be able to access different endpoint // Clean up between endpoint tests
$result = checkRateLimit($this->db, '/dashboard'); $this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
$this->assertTrue($result);
// Make requests to register endpoint (limit: 5)
for ($i = 0; $i < 5; $i++) {
$result = checkRateLimit($this->db, '/register');
$this->assertTrue($result, "Request $i to /register should be allowed");
}
// The 6th request to register should be blocked
$result = checkRateLimit($this->db, '/register');
$this->assertFalse($result, "Request should be blocked after 5 requests to /register");
} }
public function testDifferentIpAddresses() public function testDifferentIpAddresses()
{ {
// Use up rate limit for first IP // Make requests from first IP
$_SERVER['REMOTE_ADDR'] = '127.0.0.1'; for ($i = 0; $i < 30; $i++) {
for ($i = 0; $i < 5; $i++) { $result = checkRateLimit($this->db, '/login');
checkRateLimit($this->db, '/login'); $this->assertTrue($result);
} }
// Different IP should not be affected // Change IP
$_SERVER['REMOTE_ADDR'] = '127.0.0.2'; $_SERVER['REMOTE_ADDR'] = '8.8.4.4';
$result = checkRateLimit($this->db, '/login');
$this->assertTrue($result); // Should be able to make requests from different IP
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->db->execute( $this->rateLimiter->addToWhitelist('8.8.8.8', false, 'Test whitelist', 'PHPUnit');
'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 < 10; $i++) { for ($i = 0; $i < 50; $i++) {
$result = checkRateLimit($this->db, '/login'); $result = checkRateLimit($this->db, '/login');
$this->assertTrue($result); $this->assertTrue($result);
} }
@ -154,30 +252,39 @@ class RateLimitMiddlewareTest extends TestCase
public function testBlacklistedIp() public function testBlacklistedIp()
{ {
// Add IP to blacklist // Add IP to blacklist and verify it was added
$this->db->execute( $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')");
'INSERT INTO ip_blacklist (ip_address, reason, created_by) VALUES (?, ?, ?)',
['127.0.0.1', 'Test blacklist', 'PHPUnit']
);
// Should be blocked immediately // Request should be blocked
$result = checkRateLimit($this->db, '/login'); $result = checkRateLimit($this->db, '/login');
$this->assertFalse($result); $this->assertFalse($result, "Blacklisted IP should be blocked");
} }
public function testRateLimitPersistence() public function testRateLimitPersistence()
{ {
// Use up some of the rate limit // Clean any existing rate limit records
for ($i = 0; $i < 2; $i++) { $this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
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}");
} }
// Destroy and restart session // The 61st request should be blocked
//session_destroy();
//session_start();
// Should still count previous requests
$result = checkRateLimit($this->db, '/login'); $result = checkRateLimit($this->db, '/login');
$this->assertTrue($result); $this->assertFalse($result, "Request should be blocked after 60 requests");
} }
} }

View File

@ -1,8 +1,11 @@
<?php <?php
require_once dirname(__DIR__, 3) . '/app/includes/session_middleware.php';
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Feature\Middleware\Mock\Session;
use Tests\Feature\Middleware\Mock\Feedback;
require_once __DIR__ . '/MockSession.php';
require_once __DIR__ . '/MockFeedback.php';
class SessionMiddlewareTest extends TestCase class SessionMiddlewareTest extends TestCase
{ {
@ -38,11 +41,24 @@ class SessionMiddlewareTest extends TestCase
protected function tearDown(): void protected function tearDown(): void
{ {
parent::tearDown(); parent::tearDown();
$_SESSION = [];
} }
public function testSessionStart() protected function applyMiddleware()
{ {
$result = applySessionMiddleware($this->config, $this->app_root); // Check session validity
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);
@ -54,24 +70,10 @@ 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->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout'); $this->assertEmpty($_SESSION);
}
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()
@ -79,7 +81,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 = applySessionMiddleware($this->config, $this->app_root); $result = $this->applyMiddleware();
$this->assertTrue($result); $this->assertTrue($result);
$this->assertArrayHasKey('user_id', $_SESSION); $this->assertArrayHasKey('user_id', $_SESSION);
@ -88,19 +90,19 @@ class SessionMiddlewareTest extends TestCase
public function testNoUserSession() public function testNoUserSession()
{ {
unset($_SESSION['user_id']); unset($_SESSION['user_id']);
$result = applySessionMiddleware($this->config, $this->app_root); $result = $this->applyMiddleware();
$this->assertFalse($result); $this->assertFalse($result);
$this->assertArrayNotHasKey('user_id', $_SESSION); $this->assertEmpty($_SESSION);
} }
public function testSessionHeaders() public function testInvalidSession()
{ {
$_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 = applySessionMiddleware($this->config, $this->app_root); $result = $this->applyMiddleware();
$this->assertFalse($result); $this->assertFalse($result);
$this->assertArrayNotHasKey('user_id', $_SESSION, 'Session should be cleared after timeout'); $this->assertEmpty($_SESSION);
} }
} }

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($user_id, $message, $scope = 'user') { public static function insertLog($userId, $message, $scope = 'user') {
return true; return true;
} }
} }

View File

@ -24,9 +24,9 @@ class AgentTest extends TestCase
'dbFile' => ':memory:' 'dbFile' => ':memory:'
]); ]);
// Create jilo_agents table // Create jilo_agent table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE jilo_agents ( CREATE TABLE jilo_agent (
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_types table // Create jilo_agent_type table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE jilo_agent_types ( CREATE TABLE jilo_agent_type (
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 hosts table // Create host table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE hosts ( CREATE TABLE host (
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 hosts (id, platform_id, name) VALUES (1, 1, 'Test Host') INSERT INTO host (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_types (id, description, endpoint) INSERT INTO jilo_agent_type (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_agents WHERE host_id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agent 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_agents WHERE host_id = ? LIMIT 1'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agent 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_agents WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agent 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_agents WHERE host_id = ? LIMIT 1'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agent 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_agents WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agent 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_agents WHERE host_id = ? LIMIT 1'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agent 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 hosts table // Create host table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE hosts ( CREATE TABLE host (
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_agents table for relationship testing // Create jilo_agent table for relationship testing
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE jilo_agents ( CREATE TABLE jilo_agent (
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 hosts WHERE platform_id = ? AND name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM host 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 hosts WHERE platform_id = ? AND name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM host 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 hosts WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM host 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 hosts WHERE platform_id = ? AND name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT id FROM host 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_agents (host_id, agent_type_id, url, secret_key) INSERT INTO jilo_agent (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 hosts WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM host 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_agents WHERE host_id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agent 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,138 +1,261 @@
<?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' => 'sqlite', 'type' => 'mariadb',
'dbFile' => ':memory:' 'host' => $host,
'port' => '3306',
'dbname' => 'jilo_test',
'user' => 'test_jilo',
'password' => $password
]); ]);
// Create users table // Create user table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE users ( CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
username TEXT NOT NULL username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
"); ");
// Create test user // Create log table with the expected schema from Log class
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
INSERT INTO users (id, username) VALUES (1, 'testuser'), (2, 'testuser2') CREATE TABLE IF NOT EXISTS log (
"); id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
// Create logs table scope VARCHAR(50) NOT NULL DEFAULT 'user',
$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 DATETIME DEFAULT CURRENT_TIMESTAMP, time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) FOREIGN KEY (user_id) REFERENCES user(id)
) )
"); ");
$this->log = new Log($this->db); // Create test users with all required fields
$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(1, 'Test message', 'test'); $result = $this->log->insertLog($this->testUserId, 'Test message', 'test');
$this->assertTrue($result); $this->assertTrue($result);
$stmt = $this->db->getConnection()->prepare("SELECT * FROM logs WHERE scope = ?"); // Verify the log was inserted
$stmt->execute(['test']); $stmt = $this->db->getConnection()->query("SELECT * FROM log WHERE user_id = {$this->testUserId} LIMIT 1");
$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 test logs // Insert some test logs with a delay to ensure order
$this->log->insertLog(1, 'Test message 1', 'user'); $this->log->insertLog($this->testUserId, 'Test message 1', 'user');
$this->log->insertLog(1, 'Test message 2', 'user'); sleep(1); // Ensure different timestamps
$this->log->insertLog($this->testUserId, 'Test message 2', 'user');
$logs = $this->log->readLog(1, 'user'); $logs = $this->log->readLog($this->testUserId, 'user');
$this->assertCount(2, $logs); $this->assertCount(2, $logs);
$this->assertEquals('Test message 1', $logs[0]['message']); $this->assertEquals('Test message 2', $logs[0]['message']); // Most recent first (by time)
$this->assertEquals('Test message 2', $logs[1]['message']);
} }
public function testReadLogWithTimeFilter() public function testReadLogWithTimeFilter()
{ {
// Insert test logs with different times // First message from yesterday
$this->log->insertLog(1, 'Old message', 'user'); $yesterday = date('Y-m-d', strtotime('-1 day'));
sleep(1); // Ensure different timestamps $stmt = $this->db->getConnection()->prepare("
$this->log->insertLog(1, 'New message', 'user'); INSERT INTO log (user_id, scope, message, time)
VALUES (?, ?, ?, ?)
");
$stmt->execute([$this->testUserId, 'user', 'Test message 1', $yesterday . ' 12:00:00']);
$now = date('Y-m-d H:i:s'); // Second message from today
$oneHourAgo = date('Y-m-d H:i:s', strtotime('-1 hour')); $today = date('Y-m-d');
$stmt->execute([$this->testUserId, 'user', 'Test message 2', $today . ' 12:00:00']);
$logs = $this->log->readLog(1, 'user', 0, '', [ // Should get only today's messages
'from_time' => $oneHourAgo, $logs = $this->log->readLog($this->testUserId, 'user', 0, '', [
'until_time' => $now 'from_time' => $today
]); ]);
$this->assertCount(1, $logs);
$this->assertCount(2, $logs); $this->assertEquals('Test message 2', $logs[0]['message']); // Most recent first
} }
public function testReadLogWithPagination() public function testReadLogWithPagination()
{ {
// Insert test logs // Insert multiple test logs with delays to ensure order
$this->log->insertLog(1, 'Message 1', 'user'); for ($i = 1; $i <= 5; $i++) {
$this->log->insertLog(1, 'Message 2', 'user'); $this->log->insertLog($this->testUserId, "Test message $i", 'user');
$this->log->insertLog(1, 'Message 3', 'user'); sleep(1); // Ensure different timestamps
}
// Test with limit // Get all logs to verify total
$logs = $this->log->readLog(1, 'user', 0, 2); $allLogs = $this->log->readLog($this->testUserId, 'user');
$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']);
// Test with offset // Get second page (offset 2, limit 2)
$logs = $this->log->readLog(1, 'user', 2, 2); $logs = $this->log->readLog($this->testUserId, 'user', 2, 2);
$this->assertCount(1, $logs); $this->assertCount(2, $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 // Insert test logs with different messages and delays
$this->log->insertLog(1, 'Test message', 'user'); $this->log->insertLog($this->testUserId, 'Test message ABC', 'user');
$this->log->insertLog(1, 'Another message', 'user'); sleep(1); // Ensure different timestamps
$this->log->insertLog($this->testUserId, 'Test message XYZ', 'user');
sleep(1); // Ensure different timestamps
$this->log->insertLog($this->testUserId, 'Different message', 'user');
$logs = $this->log->readLog(1, 'user', 0, '', [ // Filter by message content
'message' => 'Test' $logs = $this->log->readLog($this->testUserId, 'user', 0, '', ['message' => 'Test message']);
]); $this->assertCount(2, $logs);
$this->assertCount(1, $logs); // Verify filtered results
$this->assertEquals('Test message', $logs[0]['message']); foreach ($logs as $log) {
} $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 hosts table // Create host table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE hosts ( CREATE TABLE host (
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_agents table // Create jilo_agent table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE jilo_agents ( CREATE TABLE jilo_agent (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL host_id INTEGER NOT NULL
) )
"); ");
// Create platforms table // Create platform table
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE platforms ( CREATE TABLE platform (
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 platforms WHERE name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM platform 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 platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO platform (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 platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO platform (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 platforms WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT * FROM platform 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 platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO platform (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 hosts (platform_id, name) VALUES (?, ?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO host (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_agents (host_id) VALUES (?)'); $stmt = $this->db->getConnection()->prepare('INSERT INTO jilo_agent (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 platforms WHERE id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform 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 hosts WHERE platform_id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM host 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_agents WHERE host_id = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM jilo_agent 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 platforms WHERE name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform 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 platforms WHERE name = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform 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 platforms WHERE jilo_database = ?'); $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platform 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,103 +8,99 @@ use PHPUnit\Framework\TestCase;
class RateLimiterTest extends TestCase class RateLimiterTest extends TestCase
{ {
private $rateLimiter;
private $db; private $db;
private $rateLimiter;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
// Set up in-memory SQLite database // 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
$this->db = new Database([ $this->db = new Database([
'type' => 'sqlite', 'type' => 'mariadb',
'dbFile' => ':memory:' 'host' => $host,
'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 = '127.0.0.1'; $ip = '8.8.8.8';
$username = 'testuser';
// Clean up any existing attempts first // Record some login attempts
$stmt = $this->db->getConnection()->prepare("DELETE FROM {$this->rateLimiter->authRatelimitTable} WHERE ip_address = ?"); $stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->authRatelimitTable}
$stmt->execute([$ip]); (ip_address, username, attempted_at) VALUES (?, ?, NOW())");
// 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(0, $attempts); $this->assertEquals(3, $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 testIpBlacklisting() public function testIsIpBlacklisted()
{ {
$ip = '192.0.2.1'; // Using TEST-NET-1 range $ip = '8.8.8.8';
// 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} (ip_address, reason) VALUES (?, ?)"); $stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable}
$stmt->execute([$nonBlacklistedIp, 'Test blacklist']); (ip_address, is_network, reason) VALUES (?, ?, ?)");
$stmt->execute([$ip, 0, 'Test blacklist']); // Explicitly set is_network to 0 (false)
// IP should now be blacklisted
$this->assertTrue($this->rateLimiter->isIpBlacklisted($nonBlacklistedIp));
}
public function testIpWhitelisting()
{
$ip = '127.0.0.1'; // Localhost
// Clean up any existing whitelist entries
$stmt = $this->db->getConnection()->prepare("DELETE FROM {$this->rateLimiter->whitelistTable} WHERE ip_address = ?");
$stmt->execute([$ip]);
// 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));
// 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 testIpRangeBlacklisting()
{
$ip = '8.8.8.8'; // Google DNS
$networkIp = '8.8.8.0/24'; // Network containing Google DNS
// Initially IP should not be blacklisted
$this->assertFalse($this->rateLimiter->isIpBlacklisted($ip));
// Add network to blacklist
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable} (ip_address, is_network, reason) VALUES (?, 1, ?)");
$stmt->execute([$networkIp, 'Test network blacklist']);
// IP in range should now be blacklisted
$this->assertTrue($this->rateLimiter->isIpBlacklisted($ip)); $this->assertTrue($this->rateLimiter->isIpBlacklisted($ip));
$this->assertFalse($this->rateLimiter->isIpBlacklisted('8.8.4.4'));
}
public function testIsIpWhitelisted()
{
// Test with an IP that's not in the default whitelisted ranges
$ip = '8.8.8.8'; // Google's DNS, definitely not in private ranges
// Add IP to whitelist
$stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->whitelistTable}
(ip_address, is_network, description) VALUES (?, ?, ?)");
$stmt->execute([$ip, 0, 'Test whitelist']); // Explicitly set is_network to 0 (false)
$this->assertTrue($this->rateLimiter->isIpWhitelisted($ip));
$this->assertFalse($this->rateLimiter->isIpWhitelisted('8.8.4.4')); // Another IP not in private ranges
}
public function testRateLimitCheck()
{
$ip = '8.8.8.8'; // Use non-whitelisted IP
$endpoint = '/test';
// First request should be allowed
$this->assertTrue($this->rateLimiter->isPageRequestAllowed($ip, $endpoint));
// Add requests up to the limit
for ($i = 0; $i < 60; $i++) { // Default limit is 60 per minute
$this->rateLimiter->recordPageRequest($ip, $endpoint);
}
// The next request should be rate limited
$this->assertFalse($this->rateLimiter->isPageRequestAllowed($ip, $endpoint));
} }
} }

View File

@ -0,0 +1,91 @@
<?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

@ -0,0 +1,197 @@
<?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,6 +2,7 @@
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;
@ -9,175 +10,161 @@ 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();
// Set up test database // 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([ $this->db = new Database([
'type' => 'sqlite', 'type' => 'mariadb',
'dbFile' => ':memory:' 'host' => $host,
'port' => '3306',
'dbname' => 'jilo_test',
'user' => 'test_jilo',
'password' => $password
]); ]);
// Create users table // Create user table with MariaDB syntax
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE users ( CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INT PRIMARY KEY AUTO_INCREMENT,
username TEXT NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE,
password TEXT NOT NULL password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
"); ");
// Create users_meta table // Create user_meta table with MariaDB syntax
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE users_meta ( CREATE TABLE IF NOT EXISTS user_meta (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INT PRIMARY KEY AUTO_INCREMENT,
user_id INTEGER NOT NULL, user_id INT NOT NULL,
name TEXT, name VARCHAR(255),
email TEXT, email VARCHAR(255),
timezone TEXT, timezone VARCHAR(100),
bio TEXT, bio TEXT,
avatar TEXT, avatar VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES users(id) 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 // Create user_2fa table for two-factor authentication
$this->db->getConnection()->exec(" $this->db->getConnection()->exec("
CREATE TABLE user_2fa ( CREATE TABLE IF NOT EXISTS user_2fa (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INT PRIMARY KEY AUTO_INCREMENT,
user_id INTEGER NOT NULL, user_id INT NOT NULL,
secret_key TEXT NOT NULL, secret_key VARCHAR(255) 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 users(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES user(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);
} }
public function testRegister() protected function tearDown(): void
{ {
$result = $this->user->register('testuser', 'password123'); // Drop tables in correct order
$this->assertTrue($result); $this->db->getConnection()->exec("DROP TABLE IF EXISTS user_2fa");
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth");
// Verify user was created $this->db->getConnection()->exec("DROP TABLE IF EXISTS user_meta");
$stmt = $this->db->getConnection()->prepare('SELECT * FROM users WHERE username = ?'); $this->db->getConnection()->exec("DROP TABLE IF EXISTS user");
$stmt->execute(['testuser']); parent::tearDown();
$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()
{ {
// Create a test user // First register a user
$username = 'testuser';
$password = 'password123'; $password = 'password123';
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)'); $this->register->register($username, $password);
$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('testuser', $password); $result = $this->user->login($username, $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 an exception for valid credentials: ' . $e->getMessage()); $this->fail('Login should not throw for valid credentials: ' . $e->getMessage());
} }
// Test failed login // Test failed login
try { $result = $this->user->login($username, 'wrongpassword');
$this->user->login('testuser', 'wrongpassword'); $this->assertIsArray($result);
$this->fail('Login should throw an exception for invalid credentials'); $this->assertEquals('failed', $result['status']);
} catch (Exception $e) { $this->assertArrayHasKey('message', $result);
$this->assertStringContainsString('Invalid credentials', $e->getMessage()); $this->assertStringContainsString('Invalid credentials', $result['message']);
}
// 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()
{ {
// Create a test user // Register a test user first
$stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)'); $username = 'testuser';
$stmt->execute(['testuser', 'hashedpassword']); $password = 'password123';
$userId = $this->db->getConnection()->lastInsertId(); $result = $this->register->register($username, $password);
$this->assertTrue($result);
// Create user meta with some data // Get user ID from database
$stmt = $this->db->getConnection()->prepare('INSERT INTO users_meta (user_id, name, email) VALUES (?, ?, ?)'); $stmt = $this->db->getConnection()->prepare("SELECT id FROM user WHERE username = ?");
$stmt->execute([$userId, 'Test User', 'test@example.com']); $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); $userDetails = $this->user->getUserDetails($userId);
$this->assertIsArray($userDetails);
$this->assertCount(1, $userDetails); // Should return one row
$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']);
// Test nonexistent user $this->assertIsArray($userDetails);
$userDetails = $this->user->getUserDetails(999); $this->assertNotEmpty($userDetails);
$this->assertEmpty($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

@ -12,9 +12,19 @@ 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);
@ -26,18 +36,23 @@ 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' => [ 'db_type' => 'mariadb',
'type' => 'sqlite', 'sql' => [
'dbFile' => ':memory:' 'sql_host' => 'localhost',
] '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 $dbWeb; global $db;
return [ return [
'db' => $dbWeb 'db' => $db
]; ];
} }
} }

View File

@ -26,7 +26,11 @@
</coverage> </coverage>
<php> <php>
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/> <env name="DB_TYPE" value="mariadb"/>
<env name="DB_DATABASE" value=":memory:"/> <env name="DB_HOST" value="localhost"/>
<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

@ -0,0 +1,35 @@
-- 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

@ -0,0 +1,12 @@
<?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

@ -0,0 +1,180 @@
/* 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