Compare commits
No commits in common. "main" and "v0.3" have entirely different histories.
|
@ -1,112 +0,0 @@
|
|||
name: PHP Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
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:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.2'
|
||||
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
|
||||
run: |
|
||||
cd tests
|
||||
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
|
||||
run: |
|
||||
cd tests
|
||||
./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
|
59
CHANGELOG.md
59
CHANGELOG.md
|
@ -4,65 +4,6 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
---
|
||||
|
||||
## Unreleased
|
||||
|
||||
#### Links
|
||||
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.4...HEAD
|
||||
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.4...HEAD
|
||||
- github: https://github.com/lindeas/jilo-web/compare/v0.4...HEAD
|
||||
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.4...HEAD
|
||||
|
||||
### Added
|
||||
- Added CSS and JS to the default theme
|
||||
- Added change theme menu entry
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
---
|
||||
|
||||
## 0.4 - 2025-04-12
|
||||
|
||||
#### Links
|
||||
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.3...v0.4
|
||||
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.3...v0.4
|
||||
- github: https://github.com/lindeas/jilo-web/compare/v0.3...v0.4
|
||||
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.3...v0.4
|
||||
|
||||
### Added
|
||||
- Added top-right menu with profile, admin, and docs sections
|
||||
- Added two-factor authentication
|
||||
- Added resetting of forgotten password
|
||||
- Added login credentials management
|
||||
- Added proper pagination
|
||||
- Added agents managemet pages
|
||||
- Added javascript-based feedback messages
|
||||
- Added description to each page
|
||||
- Added CSRF checks
|
||||
- Added validator class for all forms
|
||||
- Added rate limiting to all pages
|
||||
- Added authentication rate limiting to login and registration
|
||||
- Added unit tests
|
||||
- Added integration/feature tests
|
||||
- Added testing workflow for github
|
||||
|
||||
### Changed
|
||||
- Increased session to 2 hours w/out "remember me", 30 days with
|
||||
- Made the config editing in-place with AJAX
|
||||
- Redesigned the help page
|
||||
- Moved graphs and latest data to their own pages
|
||||
- Moved live config.js to its own page
|
||||
- Redesigned the messages system and renamed them to feedback messages
|
||||
|
||||
### Fixed
|
||||
- Bugfixes
|
||||
- Fixed config editing
|
||||
- Fixed logs search
|
||||
- Removed hardcoded messages, changed to feedback messages
|
||||
|
||||
---
|
||||
|
||||
## 0.3 - 2025-01-15
|
||||
|
||||
#### Links
|
||||
|
|
|
@ -24,32 +24,29 @@ class Agent {
|
|||
|
||||
|
||||
/**
|
||||
* Retrieves details of agents for a specified host.
|
||||
* Retrieves details of a specified agent ID (or all agents) in a specified platform.
|
||||
*
|
||||
* @param int $host_id The host ID to filter agents by.
|
||||
* @param int $agent_id Optional agent ID to filter by.
|
||||
* @param int $platform_id The platform ID to filter agents by.
|
||||
* @param int $agent_id The agent ID to filter by. If empty, all agents are returned.
|
||||
*
|
||||
* @return array The list of agent details.
|
||||
*/
|
||||
public function getAgentDetails($host_id, $agent_id = '') {
|
||||
public function getAgentDetails($platform_id, $agent_id = '') {
|
||||
$sql = 'SELECT
|
||||
ja.id,
|
||||
ja.host_id,
|
||||
ja.platform_id,
|
||||
ja.agent_type_id,
|
||||
ja.url,
|
||||
ja.secret_key,
|
||||
ja.check_period,
|
||||
jat.description AS agent_description,
|
||||
jat.endpoint AS agent_endpoint,
|
||||
h.platform_id
|
||||
jat.endpoint AS agent_endpoint
|
||||
FROM
|
||||
jilo_agent ja
|
||||
jilo_agents ja
|
||||
JOIN
|
||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
||||
JOIN
|
||||
host h ON ja.host_id = h.id
|
||||
jilo_agent_types jat ON ja.agent_type_id = jat.id
|
||||
WHERE
|
||||
ja.host_id = :host_id';
|
||||
platform_id = :platform_id';
|
||||
|
||||
if ($agent_id !== '') {
|
||||
$sql .= ' AND ja.id = :agent_id';
|
||||
|
@ -57,7 +54,7 @@ class Agent {
|
|||
|
||||
$query = $this->db->prepare($sql);
|
||||
|
||||
$query->bindParam(':host_id', $host_id);
|
||||
$query->bindParam(':platform_id', $platform_id);
|
||||
if ($agent_id !== '') {
|
||||
$query->bindParam(':agent_id', $agent_id);
|
||||
}
|
||||
|
@ -78,20 +75,17 @@ class Agent {
|
|||
public function getAgentIDDetails($agent_id) {
|
||||
$sql = 'SELECT
|
||||
ja.id,
|
||||
ja.host_id,
|
||||
ja.platform_id,
|
||||
ja.agent_type_id,
|
||||
ja.url,
|
||||
ja.secret_key,
|
||||
ja.check_period,
|
||||
jat.description AS agent_description,
|
||||
jat.endpoint AS agent_endpoint,
|
||||
h.platform_id
|
||||
jat.endpoint AS agent_endpoint
|
||||
FROM
|
||||
jilo_agent ja
|
||||
jilo_agents ja
|
||||
JOIN
|
||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
||||
JOIN
|
||||
host h ON ja.host_id = h.id
|
||||
jilo_agent_types jat ON ja.agent_type_id = jat.id
|
||||
WHERE
|
||||
ja.id = :agent_id';
|
||||
|
||||
|
@ -110,7 +104,7 @@ class Agent {
|
|||
*/
|
||||
public function getAgentTypes() {
|
||||
$sql = 'SELECT *
|
||||
FROM jilo_agent_type
|
||||
FROM jilo_agent_types
|
||||
ORDER BY id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute();
|
||||
|
@ -120,22 +114,22 @@ class Agent {
|
|||
|
||||
|
||||
/**
|
||||
* Retrieves agent types already configured for a specific host.
|
||||
* Retrieves agent types already configured for a specific platform.
|
||||
*
|
||||
* @param int $host_id The host ID to filter agents by.
|
||||
* @param int $platform_id The platform ID to filter agents by.
|
||||
*
|
||||
* @return array List of agent types configured for the host.
|
||||
* @return array List of agent types configured for the platform.
|
||||
*/
|
||||
public function getHostAgentTypes($host_id) {
|
||||
public function getPlatformAgentTypes($platform_id) {
|
||||
$sql = 'SELECT
|
||||
id,
|
||||
agent_type_id
|
||||
FROM
|
||||
jilo_agent
|
||||
jilo_agents
|
||||
WHERE
|
||||
host_id = :host_id';
|
||||
platform_id = :platform_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->bindParam(':host_id', $host_id);
|
||||
$query->bindParam(':platform_id', $platform_id);
|
||||
$query->execute();
|
||||
|
||||
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
@ -143,23 +137,23 @@ class Agent {
|
|||
|
||||
|
||||
/**
|
||||
* Add a new agent to the database.
|
||||
* Adds a new agent to the platform.
|
||||
*
|
||||
* @param int $host_id The host ID to add the agent to.
|
||||
* @param array $newAgent An associative array containing the details of the agent to be added.
|
||||
* @param int $platform_id The platform ID where the agent is to be added.
|
||||
* @param array $newAgent The new agent details to add.
|
||||
*
|
||||
* @return bool|string True if the agent was added successfully, otherwise error message.
|
||||
* @return bool|string Returns true on success or an error message on failure.
|
||||
*/
|
||||
public function addAgent($host_id, $newAgent) {
|
||||
public function addAgent($platform_id, $newAgent) {
|
||||
try {
|
||||
$sql = 'INSERT INTO jilo_agent
|
||||
(host_id, agent_type_id, url, secret_key, check_period)
|
||||
$sql = 'INSERT INTO jilo_agents
|
||||
(platform_id, agent_type_id, url, secret_key, check_period)
|
||||
VALUES
|
||||
(:host_id, :agent_type_id, :url, :secret_key, :check_period)';
|
||||
(:platform_id, :agent_type_id, :url, :secret_key, :check_period)';
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':host_id' => $host_id,
|
||||
':platform_id' => $platform_id,
|
||||
':agent_type_id' => $newAgent['type_id'],
|
||||
':url' => $newAgent['url'],
|
||||
':secret_key' => $newAgent['secret_key'],
|
||||
|
@ -175,34 +169,33 @@ class Agent {
|
|||
|
||||
|
||||
/**
|
||||
* Edit an existing agent in the database.
|
||||
* Edits an existing agent's details.
|
||||
*
|
||||
* @param int $agent_id The ID of the agent to edit.
|
||||
* @param array $updatedAgent An associative array containing the updated details of the agent.
|
||||
* @param int $platform_id The platform ID where the agent exists.
|
||||
* @param array $updatedAgent The updated agent details.
|
||||
*
|
||||
* @return bool|string True if the agent was updated successfully, otherwise error message.
|
||||
* @return bool|string Returns true on success or an error message on failure.
|
||||
*/
|
||||
public function editAgent($agent_id, $updatedAgent) {
|
||||
public function editAgent($platform_id, $updatedAgent) {
|
||||
try {
|
||||
$sql = 'UPDATE jilo_agent
|
||||
SET
|
||||
$sql = 'UPDATE jilo_agents SET
|
||||
agent_type_id = :agent_type_id,
|
||||
url = :url,
|
||||
secret_key = :secret_key,
|
||||
check_period = :check_period
|
||||
WHERE
|
||||
id = :agent_id';
|
||||
|
||||
// Convert empty secret key to NULL
|
||||
$secretKey = !empty($updatedAgent['secret_key']) ? $updatedAgent['secret_key'] : null;
|
||||
id = :agent_id
|
||||
AND
|
||||
platform_id = :platform_id';
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':agent_id' => $agent_id,
|
||||
':agent_type_id' => $updatedAgent['agent_type_id'],
|
||||
':url' => $updatedAgent['url'],
|
||||
':secret_key' => $secretKey,
|
||||
':secret_key' => $updatedAgent['secret_key'],
|
||||
':check_period' => $updatedAgent['check_period'],
|
||||
':agent_id' => $updatedAgent['id'],
|
||||
':platform_id' => $platform_id,
|
||||
]);
|
||||
|
||||
return true;
|
||||
|
@ -214,7 +207,7 @@ class Agent {
|
|||
|
||||
|
||||
/**
|
||||
* Deletes an agent from the database.
|
||||
* Deletes an agent from the platform.
|
||||
*
|
||||
* @param int $agent_id The agent ID to delete.
|
||||
*
|
||||
|
@ -222,7 +215,7 @@ class Agent {
|
|||
*/
|
||||
public function deleteAgent($agent_id) {
|
||||
try {
|
||||
$sql = 'DELETE FROM jilo_agent
|
||||
$sql = 'DELETE FROM jilo_agents
|
||||
WHERE
|
||||
id = :agent_id';
|
||||
|
||||
|
@ -286,7 +279,7 @@ class Agent {
|
|||
$base64Url_payload = $this->base64UrlEncode($payload);
|
||||
|
||||
// signature
|
||||
$signature = hash_hmac('sha256', $base64Url_header . "." . $base64Url_payload, $secret_key ?? '', true);
|
||||
$signature = hash_hmac('sha256', $base64Url_header . "." . $base64Url_payload, $secret_key, true);
|
||||
$base64Url_signature = $this->base64UrlEncode($signature);
|
||||
|
||||
// build the JWT
|
||||
|
@ -405,30 +398,28 @@ class Agent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieves the latest stored data for a specific host, agent type, and metric type.
|
||||
* Retrieves the latest stored data for a specific platform, agent type, and metric type.
|
||||
*
|
||||
* @param int $host_id The host ID.
|
||||
* @param int $platform_id The platform ID.
|
||||
* @param string $agent_type The agent type.
|
||||
* @param string $metric_type The metric type to filter by.
|
||||
*
|
||||
* @return mixed The latest stored data.
|
||||
*/
|
||||
public function getLatestData($host_id, $agent_type, $metric_type) {
|
||||
public function getLatestData($platform_id, $agent_type, $metric_type) {
|
||||
$sql = 'SELECT
|
||||
jac.timestamp,
|
||||
jac.response_content,
|
||||
jac.agent_id,
|
||||
jat.description
|
||||
FROM
|
||||
jilo_agent_check jac
|
||||
jilo_agent_checks jac
|
||||
JOIN
|
||||
jilo_agent ja ON jac.agent_id = ja.id
|
||||
jilo_agents ja ON jac.agent_id = ja.id
|
||||
JOIN
|
||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
||||
JOIN
|
||||
host h ON ja.host_id = h.id
|
||||
jilo_agent_types jat ON ja.agent_type_id = jat.id
|
||||
WHERE
|
||||
h.id = :host_id
|
||||
ja.platform_id = :platform_id
|
||||
AND jat.description = :agent_type
|
||||
AND jac.status_code = 200
|
||||
ORDER BY
|
||||
|
@ -437,7 +428,7 @@ class Agent {
|
|||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':host_id' => $host_id,
|
||||
':platform_id' => $platform_id,
|
||||
':agent_type' => $agent_type
|
||||
]);
|
||||
|
||||
|
@ -506,29 +497,27 @@ class Agent {
|
|||
/**
|
||||
* Gets historical data for a specific metric from agent checks
|
||||
*
|
||||
* @param int $host_id The host ID
|
||||
* @param int $platform_id The platform ID
|
||||
* @param string $agent_type The type of agent (e.g., 'jvb', 'jicofo')
|
||||
* @param string $metric_type The type of metric to retrieve
|
||||
* @param string $from_time Start time in Y-m-d format
|
||||
* @param string $until_time End time in Y-m-d format
|
||||
* @return array Array with the dataset from agent checks
|
||||
*/
|
||||
public function getHistoricalData($host_id, $agent_type, $metric_type, $from_time, $until_time) {
|
||||
public function getHistoricalData($platform_id, $agent_type, $metric_type, $from_time, $until_time) {
|
||||
// Get data from agent checks
|
||||
$sql = 'SELECT
|
||||
DATE(jac.timestamp) as date,
|
||||
jac.response_content,
|
||||
COUNT(*) as checks_count
|
||||
FROM
|
||||
jilo_agent_check jac
|
||||
jilo_agent_checks jac
|
||||
JOIN
|
||||
jilo_agent ja ON jac.agent_id = ja.id
|
||||
jilo_agents ja ON jac.agent_id = ja.id
|
||||
JOIN
|
||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
||||
JOIN
|
||||
host h ON ja.host_id = h.id
|
||||
jilo_agent_types jat ON ja.agent_type_id = jat.id
|
||||
WHERE
|
||||
h.id = :host_id
|
||||
ja.platform_id = :platform_id
|
||||
AND jat.description = :agent_type
|
||||
AND jac.status_code = 200
|
||||
AND DATE(jac.timestamp) BETWEEN :from_time AND :until_time
|
||||
|
@ -539,7 +528,7 @@ class Agent {
|
|||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':host_id' => $host_id,
|
||||
':platform_id' => $platform_id,
|
||||
':agent_type' => $agent_type,
|
||||
':from_time' => $from_time,
|
||||
':until_time' => $until_time
|
||||
|
@ -576,72 +565,6 @@ class Agent {
|
|||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the previous record for a specific metric
|
||||
*
|
||||
* @param int $host_id The host ID
|
||||
* @param string $agent_type The type of agent (e.g., 'jvb', 'jicofo')
|
||||
* @param string $metric_type The type of metric to retrieve
|
||||
* @param string $current_timestamp Current record's timestamp to get data before this
|
||||
* @return array|null Previous record data or null if not found
|
||||
*/
|
||||
public function getPreviousRecord($host_id, $agent_type, $metric_type, $current_timestamp) {
|
||||
$sql = 'SELECT
|
||||
jac.timestamp,
|
||||
jac.response_content
|
||||
FROM
|
||||
jilo_agent_check jac
|
||||
JOIN
|
||||
jilo_agent ja ON jac.agent_id = ja.id
|
||||
JOIN
|
||||
jilo_agent_type jat ON ja.agent_type_id = jat.id
|
||||
JOIN
|
||||
host h ON ja.host_id = h.id
|
||||
WHERE
|
||||
h.id = :host_id
|
||||
AND jat.description = :agent_type
|
||||
AND jac.status_code = 200
|
||||
AND jac.timestamp < :current_timestamp
|
||||
ORDER BY
|
||||
jac.timestamp DESC
|
||||
LIMIT 1';
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':host_id' => $host_id,
|
||||
':agent_type' => $agent_type,
|
||||
':current_timestamp' => $current_timestamp
|
||||
]);
|
||||
|
||||
$result = $query->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($result) {
|
||||
$json_data = json_decode($result['response_content'], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$api_data = [];
|
||||
if ($agent_type === 'jvb') {
|
||||
$api_data = $json_data['jvb_api_data'] ?? [];
|
||||
} elseif ($agent_type === 'jicofo') {
|
||||
$api_data = $json_data['jicofo_api_data'] ?? [];
|
||||
} elseif ($agent_type === 'jigasi') {
|
||||
$api_data = $json_data['jigasi_api_data'] ?? [];
|
||||
} elseif ($agent_type === 'prosody') {
|
||||
$api_data = $json_data['prosody_api_data'] ?? [];
|
||||
} elseif ($agent_type === 'nginx') {
|
||||
$api_data = $json_data['nginx_api_data'] ?? [];
|
||||
}
|
||||
|
||||
$value = $this->getNestedValue($api_data, $metric_type);
|
||||
if ($value !== null) {
|
||||
return [
|
||||
'value' => $value,
|
||||
'timestamp' => $result['timestamp']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* API Response Handler
|
||||
* Provides a consistent way to send JSON responses from controllers
|
||||
*/
|
||||
class ApiResponse {
|
||||
/**
|
||||
* Send a success response
|
||||
* @param mixed $data Optional data to include in response
|
||||
* @param string $message Optional success message
|
||||
* @param int $status HTTP status code
|
||||
*/
|
||||
public static function success($data = null, $message = '', $status = 200) {
|
||||
self::send([
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'message' => $message
|
||||
], $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an error response
|
||||
* @param string $message Error message
|
||||
* @param mixed $errors Optional error details
|
||||
* @param int $status HTTP status code
|
||||
*/
|
||||
public static function error($message, $errors = null, $status = 400) {
|
||||
self::send([
|
||||
'success' => false,
|
||||
'error' => $message,
|
||||
'errors' => $errors
|
||||
], $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the actual JSON response
|
||||
* @param array $data Response data
|
||||
* @param int $status HTTP status code
|
||||
*/
|
||||
private static function send($data, $status) {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
}
|
|
@ -36,134 +36,59 @@ class Component {
|
|||
* @return array The list of Jitsi component events or an empty array if no results.
|
||||
*/
|
||||
public function jitsiComponents($jitsi_component, $component_id, $event_type, $from_time, $until_time, $offset=0, $items_per_page='') {
|
||||
global $logObject;
|
||||
try {
|
||||
// Add time part to dates if not present
|
||||
if (strlen($from_time) <= 10) {
|
||||
$from_time .= ' 00:00:00';
|
||||
|
||||
// time period drill-down
|
||||
// FIXME make it similar to the bash version
|
||||
if (empty($from_time)) {
|
||||
$from_time = '0000-01-01';
|
||||
}
|
||||
if (strlen($until_time) <= 10) {
|
||||
$until_time .= ' 23:59:59';
|
||||
if (empty($until_time)) {
|
||||
$until_time = '9999-12-31';
|
||||
}
|
||||
$from_time = htmlspecialchars(strip_tags($from_time));
|
||||
$until_time = htmlspecialchars(strip_tags($until_time));
|
||||
|
||||
// list of jitsi component events
|
||||
$sql = "SELECT jitsi_component, loglevel, time, component_id, event_type, event_param
|
||||
FROM jitsi_components
|
||||
WHERE time >= :from_time
|
||||
AND time <= :until_time";
|
||||
|
||||
// Only add component and event filters if they're not the default values
|
||||
if ($jitsi_component !== 'jitsi_component') {
|
||||
$sql .= " AND LOWER(jitsi_component) = LOWER(:jitsi_component)";
|
||||
$sql = "
|
||||
SELECT
|
||||
jitsi_component, loglevel, time, component_id, event_type, event_param
|
||||
FROM
|
||||
jitsi_components
|
||||
WHERE
|
||||
jitsi_component = %s
|
||||
AND
|
||||
component_id = %s";
|
||||
if ($event_type != '' && $event_type != 'event_type') {
|
||||
$sql .= "
|
||||
AND
|
||||
event_type LIKE '%%%s%%'";
|
||||
}
|
||||
if ($component_id !== 'component_id') {
|
||||
$sql .= " AND component_id = :component_id";
|
||||
}
|
||||
if ($event_type !== 'event_type') {
|
||||
$sql .= " AND event_type LIKE :event_type";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY time";
|
||||
$sql .= "
|
||||
AND
|
||||
(time >= '%s 00:00:00' AND time <= '%s 23:59:59')
|
||||
ORDER BY
|
||||
time";
|
||||
|
||||
if ($items_per_page) {
|
||||
$sql .= ' LIMIT :offset, :items_per_page';
|
||||
$items_per_page = (int)$items_per_page;
|
||||
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
// Bind parameters only if they're not default values
|
||||
if ($jitsi_component !== 'jitsi_component') {
|
||||
$stmt->bindValue(':jitsi_component', trim($jitsi_component, "'"));
|
||||
}
|
||||
if ($component_id !== 'component_id') {
|
||||
$stmt->bindValue(':component_id', trim($component_id, "'"));
|
||||
}
|
||||
if ($event_type !== 'event_type') {
|
||||
$stmt->bindValue(':event_type', '%' . trim($event_type, "'") . '%');
|
||||
// FIXME this needs to be done with bound params instead of sprintf
|
||||
if ($event_type != '' && $event_type != 'event_type') {
|
||||
$sql = sprintf($sql, $jitsi_component, $component_id, $event_type, $from_time, $until_time);
|
||||
$sql = str_replace("LIKE '%'", "LIKE '%", $sql);
|
||||
$sql = str_replace("'%'\nAND", "%' AND", $sql);
|
||||
} else {
|
||||
$sql = sprintf($sql, $jitsi_component, $component_id, $from_time, $until_time);
|
||||
}
|
||||
|
||||
$stmt->bindParam(':from_time', $from_time);
|
||||
$stmt->bindParam(':until_time', $until_time);
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute();
|
||||
|
||||
if ($items_per_page) {
|
||||
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':items_per_page', $items_per_page, PDO::PARAM_INT);
|
||||
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($result)) {
|
||||
$logObject->log('info', "Retrieved " . count($result) . " Jitsi component events", ['user_id' => $userId, 'scope' => 'system']);
|
||||
}
|
||||
return $result;
|
||||
} catch (PDOException $e) {
|
||||
$logObject->log('error', "Failed to retrieve Jitsi component events: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of components events matching the filter criteria
|
||||
*
|
||||
* @param string $jitsi_component The Jitsi component name.
|
||||
* @param int $component_id The component ID.
|
||||
* @param string $event_type The type of event to filter by.
|
||||
* @param string $from_time The start date in 'YYYY-MM-DD' format.
|
||||
* @param string $until_time The end date in 'YYYY-MM-DD' format.
|
||||
*
|
||||
* @return int The total count of matching components
|
||||
*/
|
||||
public function getComponentEventsCount($jitsi_component, $component_id, $event_type, $from_time, $until_time) {
|
||||
global $logObject;
|
||||
try {
|
||||
// Add time part to dates if not present
|
||||
if (strlen($from_time) <= 10) {
|
||||
$from_time .= ' 00:00:00';
|
||||
}
|
||||
if (strlen($until_time) <= 10) {
|
||||
$until_time .= ' 23:59:59';
|
||||
}
|
||||
|
||||
// Build the query
|
||||
$sql = "SELECT COUNT(*) as total
|
||||
FROM jitsi_components
|
||||
WHERE time >= :from_time
|
||||
AND time <= :until_time";
|
||||
|
||||
// Only add component and event filters if they're not the default values
|
||||
if ($jitsi_component !== 'jitsi_component') {
|
||||
$sql .= " AND LOWER(jitsi_component) = LOWER(:jitsi_component)";
|
||||
}
|
||||
if ($component_id !== 'component_id') {
|
||||
$sql .= " AND component_id = :component_id";
|
||||
}
|
||||
if ($event_type !== 'event_type') {
|
||||
$sql .= " AND event_type LIKE :event_type";
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
// Bind parameters only if they're not default values
|
||||
if ($jitsi_component !== 'jitsi_component') {
|
||||
$stmt->bindValue(':jitsi_component', trim($jitsi_component, "'"));
|
||||
}
|
||||
if ($component_id !== 'component_id') {
|
||||
$stmt->bindValue(':component_id', trim($component_id, "'"));
|
||||
}
|
||||
if ($event_type !== 'event_type') {
|
||||
$stmt->bindValue(':event_type', '%' . trim($event_type, "'") . '%');
|
||||
}
|
||||
|
||||
$stmt->bindParam(':from_time', $from_time);
|
||||
$stmt->bindParam(':until_time', $until_time);
|
||||
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return (int)$result['total'];
|
||||
} catch (PDOException $e) {
|
||||
$logObject->log('error', "Failed to retrieve component events count: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -382,3 +382,5 @@ SELECT COUNT(*) AS conferences
|
|||
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -3,156 +3,151 @@
|
|||
/**
|
||||
* class Config
|
||||
*
|
||||
* Handles editing and fetching of the config files.
|
||||
* Handles editing and fetching configuration files.
|
||||
*/
|
||||
class Config {
|
||||
|
||||
/**
|
||||
* Edits a config file by updating specified options.
|
||||
* Edits a configuration file by updating specified options.
|
||||
*
|
||||
* @param array $updatedConfig Key-value pairs of config options to update.
|
||||
* @param string $config_file Path to the config file.
|
||||
* @param array $updatedConfig Key-value pairs of configuration options to update.
|
||||
* @param string $config_file Path to the configuration file.
|
||||
*
|
||||
* @return array Returns an array with 'success' and 'updated' keys on success, or 'success' and 'error' keys on failure.
|
||||
* @return mixed Returns true on success, or an error message on failure.
|
||||
*/
|
||||
public function editConfigFile($updatedConfig, $config_file) {
|
||||
global $logObject, $userId;
|
||||
$allLogs = [];
|
||||
$updated = [];
|
||||
|
||||
try {
|
||||
if (!is_array($updatedConfig)) {
|
||||
throw new Exception("Invalid config data: expected array");
|
||||
}
|
||||
|
||||
if (!file_exists($config_file) || !is_writable($config_file)) {
|
||||
throw new Exception("Config file does not exist or is not writable: $config_file");
|
||||
}
|
||||
|
||||
// First we get a fresh config file contents as text
|
||||
// first we get a fresh config file contents as text
|
||||
$config_contents = file_get_contents($config_file);
|
||||
if ($config_contents === false) {
|
||||
throw new Exception("Failed to read the config file: $config_file");
|
||||
if (!$config_contents) {
|
||||
return "Failed to read the config file \"$config_file\".";
|
||||
}
|
||||
|
||||
$lines = explode("\n", $config_contents);
|
||||
|
||||
// We loop through the variables and update them
|
||||
// loop through the variables and updated them
|
||||
foreach ($updatedConfig as $key => $newValue) {
|
||||
if (strpos($key, '[') !== false) {
|
||||
preg_match_all('/([^\[\]]+)/', $key, $matches);
|
||||
if (empty($matches[1])) continue;
|
||||
// we look for 'option' => value
|
||||
// option is always in single quotes
|
||||
// value is without quotes, because it could be true/false
|
||||
$pattern = "/(['\"]{$key}['\"]\s*=>\s*)([^,]+),/";
|
||||
|
||||
$parts = $matches[1];
|
||||
$currentPath = [];
|
||||
$found = false;
|
||||
$inTargetArray = false;
|
||||
|
||||
foreach ($lines as $i => $line) {
|
||||
$line = rtrim($line);
|
||||
|
||||
if (preg_match("/^\\s*\\]/", $line)) {
|
||||
if (!empty($currentPath)) {
|
||||
if ($inTargetArray && end($currentPath) === $parts[0]) {
|
||||
$inTargetArray = false;
|
||||
}
|
||||
array_pop($currentPath);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match("/^\\s*['\"]([^'\"]+)['\"]\\s*=>/", $line, $matches)) {
|
||||
$key = $matches[1];
|
||||
|
||||
if (strpos($line, '[') !== false) {
|
||||
$currentPath[] = $key;
|
||||
if ($key === $parts[0]) {
|
||||
$inTargetArray = true;
|
||||
}
|
||||
} else if ($key === end($parts) && $inTargetArray) {
|
||||
$pathMatches = true;
|
||||
$expectedPath = array_slice($parts, 0, -1);
|
||||
|
||||
if (count($currentPath) === count($expectedPath)) {
|
||||
for ($j = 0; $j < count($expectedPath); $j++) {
|
||||
if ($currentPath[$j] !== $expectedPath[$j]) {
|
||||
$pathMatches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($pathMatches) {
|
||||
if ($newValue === 'true' || $newValue === '1') {
|
||||
// prepare the value, make booleans w/out single quotes
|
||||
if ($newValue === 'true') {
|
||||
$replacementValue = 'true';
|
||||
} elseif ($newValue === 'false' || $newValue === '0') {
|
||||
} elseif ($newValue === 'false') {
|
||||
$replacementValue = 'false';
|
||||
} else {
|
||||
$replacementValue = var_export($newValue, true);
|
||||
}
|
||||
|
||||
if (preg_match("/^(\\s*['\"]" . preg_quote($key, '/') . "['\"]\\s*=>\\s*).*?(,?)\\s*$/", $line, $matches)) {
|
||||
$lines[$i] = $matches[1] . $replacementValue . $matches[2];
|
||||
$updated[] = implode('.', array_merge($currentPath, [$key]));
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// value replacing
|
||||
$config_contents = preg_replace($pattern, "$1{$replacementValue},", $config_contents);
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$allLogs[] = "Failed to update: $key";
|
||||
}
|
||||
} else {
|
||||
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $key)) {
|
||||
throw new Exception("Invalid config key format: $key");
|
||||
// write the new config file
|
||||
if (!file_put_contents($config_file, $config_contents)) {
|
||||
return "Failed to write the config file \"$config_file\".";
|
||||
}
|
||||
|
||||
if ($newValue === 'true' || $newValue === '1') {
|
||||
$replacementValue = 'true';
|
||||
} elseif ($newValue === 'false' || $newValue === '0') {
|
||||
$replacementValue = 'false';
|
||||
} else {
|
||||
$replacementValue = var_export($newValue, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
$found = false;
|
||||
foreach ($lines as $i => $line) {
|
||||
if (preg_match("/^(\\s*['\"]" . preg_quote($key, '/') . "['\"]\\s*=>\\s*).*?(,?)\\s*$/", $line, $matches)) {
|
||||
$lines[$i] = $matches[1] . $replacementValue . $matches[2];
|
||||
$updated[] = $key;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$allLogs[] = "Failed to update: $key";
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Loads the config.js file from the Jitsi server.
|
||||
*
|
||||
* @param string $jitsiUrl The base URL of the Jitsi server.
|
||||
* @param bool $raw Whether to return the full file (true) or only uncommented values (false).
|
||||
*
|
||||
* @return string The content of the config.js file or an error message.
|
||||
*/
|
||||
public function getPlatformConfigjs($jitsiUrl, $raw = false) {
|
||||
// constructing the URL
|
||||
$configjsFile = $jitsiUrl . '/config.js';
|
||||
|
||||
// We write the new config file
|
||||
$new_contents = implode("\n", $lines);
|
||||
if (file_put_contents($config_file, $new_contents) === false) {
|
||||
throw new Exception("Failed to write the config file: $config_file");
|
||||
}
|
||||
// default content, if we can't get the file contents
|
||||
$platformConfigjs = "The file $configjsFile can't be loaded.";
|
||||
|
||||
if (!empty($allLogs)) {
|
||||
$logObject->log('info', implode("\n", $allLogs), ['user_id' => $userId, 'scope' => 'system']);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'updated' => $updated
|
||||
// ssl options
|
||||
$contextOptions = [
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$logObject->log('error', "Config update error: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
$context = stream_context_create($contextOptions);
|
||||
|
||||
// get the file
|
||||
$fileContent = @file_get_contents($configjsFile, false, $context);
|
||||
|
||||
if ($fileContent !== false) {
|
||||
|
||||
// when we need only uncommented values
|
||||
if ($raw === false) {
|
||||
// remove block comments
|
||||
$platformConfigjs = preg_replace('!/\*.*?\*/!s', '', $fileContent);
|
||||
// remove single-line comments
|
||||
$platformConfigjs = preg_replace('/\/\/[^\n]*/', '', $platformConfigjs);
|
||||
// remove empty lines
|
||||
$platformConfigjs = preg_replace('/^\s*[\r\n]/m', '', $platformConfigjs);
|
||||
|
||||
// when we need the full file as it is
|
||||
} else {
|
||||
$platformConfigjs = $fileContent;
|
||||
}
|
||||
}
|
||||
|
||||
return $platformConfigjs;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the interface_config.js file from the Jitsi server.
|
||||
*
|
||||
* @param string $jitsiUrl The base URL of the Jitsi server.
|
||||
* @param bool $raw Whether to return the full file (true) or only uncommented values (false).
|
||||
*
|
||||
* @return string The content of the interface_config.js file or an error message.
|
||||
*/
|
||||
public function getPlatformInterfaceConfigjs($jitsiUrl, $raw = false) {
|
||||
// constructing the URL
|
||||
$interfaceConfigjsFile = $jitsiUrl . '/interface_config.js';
|
||||
|
||||
// default content, if we can't get the file contents
|
||||
$platformInterfaceConfigjs = "The file $interfaceConfigjsFile can't be loaded.";
|
||||
|
||||
// ssl options
|
||||
$contextOptions = [
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
];
|
||||
$context = stream_context_create($contextOptions);
|
||||
|
||||
// get the file
|
||||
$fileContent = @file_get_contents($interfaceConfigjsFile, false, $context);
|
||||
|
||||
if ($fileContent !== false) {
|
||||
|
||||
// when we need only uncommented values
|
||||
if ($raw === false) {
|
||||
// remove block comments
|
||||
$platformInterfaceConfigjs = preg_replace('!/\*.*?\*/!s', '', $fileContent);
|
||||
// remove single-line comments
|
||||
$platformInterfaceConfigjs = preg_replace('/\/\/[^\n]*/', '', $platformInterfaceConfigjs);
|
||||
// remove empty lines
|
||||
$platformInterfaceConfigjs = preg_replace('/^\s*[\r\n]/m', '', $platformInterfaceConfigjs);
|
||||
|
||||
// when we need the full file as it is
|
||||
} else {
|
||||
$platformInterfaceConfigjs = $fileContent;
|
||||
}
|
||||
}
|
||||
|
||||
return $platformInterfaceConfigjs;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -29,12 +29,12 @@ class Database {
|
|||
public function __construct($options) {
|
||||
// check if PDO extension is loaded
|
||||
if ( !extension_loaded('pdo') ) {
|
||||
throw new Exception('PDO extension not loaded.');
|
||||
$error = getError('PDO extension not loaded.');
|
||||
}
|
||||
|
||||
// options check
|
||||
if (empty($options['type'])) {
|
||||
throw new Exception('Database type is not set.');
|
||||
$error = getError('Database type is not set.');
|
||||
}
|
||||
|
||||
// connect based on database type
|
||||
|
@ -42,15 +42,15 @@ class Database {
|
|||
case 'sqlite':
|
||||
$this->connectSqlite($options);
|
||||
break;
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
case 'mysql' || 'mariadb':
|
||||
$this->connectMysql($options);
|
||||
break;
|
||||
default:
|
||||
$this->pdo = null;
|
||||
$error = getError("Database type \"{$options['type']}\" is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Establishes a connection to a SQLite database.
|
||||
*
|
||||
|
@ -62,17 +62,12 @@ class Database {
|
|||
private function connectSqlite($options) {
|
||||
// pdo_sqlite extension is needed
|
||||
if (!extension_loaded('pdo_sqlite')) {
|
||||
throw new Exception('PDO extension for SQLite not loaded.');
|
||||
$error = getError('PDO extension for SQLite not loaded.');
|
||||
}
|
||||
|
||||
// SQLite options
|
||||
if (empty($options['dbFile'])) {
|
||||
throw new Exception('SQLite database file path is missing.');
|
||||
}
|
||||
|
||||
// For in-memory database (especially for the tests), skip file check
|
||||
if ($options['dbFile'] !== ':memory:' && !file_exists($options['dbFile'])) {
|
||||
throw new Exception("SQLite database file \"{$options['dbFile']}\" not found.");
|
||||
if (empty($options['dbFile']) || !file_exists($options['dbFile'])) {
|
||||
$error = getError("SQLite database file \"{$options['dbFile']}\" not found.");
|
||||
}
|
||||
|
||||
// connect to SQLite
|
||||
|
@ -82,10 +77,11 @@ class Database {
|
|||
// enable foreign key constraints (not ON by default in SQLite3)
|
||||
$this->pdo->exec('PRAGMA foreign_keys = ON;');
|
||||
} catch (PDOException $e) {
|
||||
throw new Exception('SQLite connection failed: ' . $e->getMessage());
|
||||
$error = getError('SQLite connection failed: ', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Establishes a connection to a MySQL (or MariaDB) database.
|
||||
*
|
||||
|
@ -101,25 +97,25 @@ class Database {
|
|||
private function connectMysql($options) {
|
||||
// pdo_mysql extension is needed
|
||||
if (!extension_loaded('pdo_mysql')) {
|
||||
throw new Exception('PDO extension for MySQL not loaded.');
|
||||
$error = getError('PDO extension for MySQL not loaded.');
|
||||
}
|
||||
|
||||
// MySQL options
|
||||
if (empty($options['host']) || empty($options['dbname']) || empty($options['user'])) {
|
||||
throw new Exception('MySQL connection data is missing.');
|
||||
$error = getError('MySQL connection data is missing.');
|
||||
}
|
||||
|
||||
// Connect to MySQL
|
||||
try {
|
||||
$port = $options['port'] ?? 3306;
|
||||
$dsn = "mysql:host={$options['host']};port={$port};dbname={$options['dbname']};charset=utf8";
|
||||
$dsn = "mysql:host={$options['host']};port={$options['port']};dbname={$options['dbname']};charset=utf8";
|
||||
$this->pdo = new PDO($dsn, $options['user'], $options['password'] ?? '');
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
$this->pdo = null;
|
||||
$error = getError('MySQL connection failed: ', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the current PDO connection instance.
|
||||
*
|
||||
|
@ -129,95 +125,6 @@ class Database {
|
|||
return $this->pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an SQL query with optional parameters.
|
||||
*
|
||||
* @param string $query The SQL query to execute
|
||||
* @param array $params Optional parameters for the query
|
||||
* @return PDOStatement|false The result of the query execution
|
||||
* @throws Exception If the query fails
|
||||
*/
|
||||
public function execute($query, $params = []) {
|
||||
if (!$this->pdo) {
|
||||
throw new Exception('No database connection.');
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($query);
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
} catch (PDOException $e) {
|
||||
throw new Exception('Query execution failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares an SQL statement for execution.
|
||||
*
|
||||
* @param string $query The SQL query to prepare
|
||||
* @return PDOStatement The prepared statement
|
||||
* @throws Exception If the preparation fails
|
||||
*/
|
||||
public function prepare($query) {
|
||||
if (!$this->pdo) {
|
||||
throw new Exception('No database connection.');
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->pdo->prepare($query);
|
||||
} catch (PDOException $e) {
|
||||
throw new Exception('Statement preparation failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins a database transaction.
|
||||
*
|
||||
* @throws Exception If starting the transaction fails
|
||||
*/
|
||||
public function beginTransaction() {
|
||||
if (!$this->pdo) {
|
||||
throw new Exception('No database connection.');
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->pdo->beginTransaction();
|
||||
} catch (PDOException $e) {
|
||||
throw new Exception('Failed to start transaction: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the current database transaction.
|
||||
*
|
||||
* @throws Exception If committing the transaction fails
|
||||
*/
|
||||
public function commit() {
|
||||
if (!$this->pdo) {
|
||||
throw new Exception('No database connection.');
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->pdo->commit();
|
||||
} catch (PDOException $e) {
|
||||
throw new Exception('Failed to commit transaction: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls back the current database transaction.
|
||||
*
|
||||
* @throws Exception If rolling back the transaction fails
|
||||
*/
|
||||
public function rollBack() {
|
||||
if (!$this->pdo) {
|
||||
throw new Exception('No database connection.');
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->pdo->rollBack();
|
||||
} catch (PDOException $e) {
|
||||
throw new Exception('Failed to rollback transaction: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -34,10 +34,11 @@ class Host {
|
|||
$sql = 'SELECT
|
||||
id,
|
||||
address,
|
||||
port,
|
||||
platform_id,
|
||||
name
|
||||
FROM
|
||||
host';
|
||||
hosts';
|
||||
|
||||
if ($platform_id !== '' && $host_id !== '') {
|
||||
$sql .= ' WHERE platform_id = :platform_id AND id = :host_id';
|
||||
|
@ -71,14 +72,15 @@ class Host {
|
|||
*/
|
||||
public function addHost($newHost) {
|
||||
try {
|
||||
$sql = 'INSERT INTO host
|
||||
(address, platform_id, name)
|
||||
$sql = 'INSERT INTO hosts
|
||||
(address, port, platform_id, name)
|
||||
VALUES
|
||||
(:address, :platform_id, :name)';
|
||||
(:address, :port, :platform_id, :name)';
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':address' => $newHost['address'],
|
||||
':port' => $newHost['port'],
|
||||
':platform_id' => $newHost['platform_id'],
|
||||
':name' => $newHost['name'],
|
||||
]);
|
||||
|
@ -97,28 +99,25 @@ class Host {
|
|||
* @param string $platform_id The platform ID to which the host belongs.
|
||||
* @param array $updatedHost An associative array containing the updated details of the host.
|
||||
*
|
||||
* @return bool|string True if the host was updated successfully, otherwise error message.
|
||||
* @return bool True if the host was updated successfully, otherwise false.
|
||||
*/
|
||||
public function editHost($platform_id, $updatedHost) {
|
||||
try {
|
||||
$sql = 'UPDATE host SET
|
||||
$sql = 'UPDATE hosts SET
|
||||
address = :address,
|
||||
port = :port,
|
||||
name = :name
|
||||
WHERE
|
||||
id = :id AND platform_id = :platform_id';
|
||||
id = :id';
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':id' => $updatedHost['id'],
|
||||
':platform_id' => $platform_id,
|
||||
':address' => $updatedHost['address'],
|
||||
':name' => $updatedHost['name']
|
||||
':port' => $updatedHost['port'],
|
||||
':name' => $updatedHost['name'],
|
||||
]);
|
||||
|
||||
if ($query->rowCount() === 0) {
|
||||
return "No host found with ID {$updatedHost['id']} in platform $platform_id";
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
|
@ -136,30 +135,21 @@ class Host {
|
|||
*/
|
||||
public function deleteHost($host_id) {
|
||||
try {
|
||||
// Start transaction
|
||||
$this->db->beginTransaction();
|
||||
$sql = 'DELETE FROM hosts
|
||||
WHERE
|
||||
id = :host_id';
|
||||
|
||||
// First delete all agents associated with this host
|
||||
$sql = 'DELETE FROM jilo_agent WHERE host_id = :host_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->bindParam(':host_id', $host_id);
|
||||
$query->execute();
|
||||
|
||||
// Then delete the host
|
||||
$sql = 'DELETE FROM host WHERE id = :host_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->bindParam(':host_id', $host_id);
|
||||
$query->execute();
|
||||
|
||||
// Commit transaction
|
||||
$this->db->commit();
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Rollback transaction on error
|
||||
$this->db->rollBack();
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,40 +1,95 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Log wrapper that delegates to plugin Log or NullLogger fallback.
|
||||
* Used when code does require_once '../app/classes/log.php'.
|
||||
* class Log
|
||||
*
|
||||
* 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 {
|
||||
private $logger;
|
||||
/**
|
||||
* @var PDO|null $db The database connection instance.
|
||||
*/
|
||||
private $db;
|
||||
|
||||
/**
|
||||
* @param mixed $database Database or DatabaseConnector instance
|
||||
* Logs constructor.
|
||||
* Initializes the database connection.
|
||||
*
|
||||
* @param object $database The database object to initialize the connection.
|
||||
*/
|
||||
public function __construct($database) {
|
||||
global $logObject;
|
||||
if (isset($logObject) && method_exists($logObject, 'insertLog')) {
|
||||
$this->logger = $logObject;
|
||||
} else {
|
||||
$this->logger = new \App\Core\NullLogger();
|
||||
$this->db = $database->getConnection();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert a log event into the database.
|
||||
*
|
||||
* @param int $user_id The ID of the user associated with the log event.
|
||||
* @param string $message The log message to insert.
|
||||
* @param string $scope The scope of the log event (e.g., 'user', 'system'). Default is 'user'.
|
||||
*
|
||||
* @return bool|string True on success, or an error message on failure.
|
||||
*/
|
||||
public function insertLog($user_id, $message, $scope='user') {
|
||||
try {
|
||||
$sql = 'INSERT INTO logs
|
||||
(user_id, scope, message)
|
||||
VALUES
|
||||
(:user_id, :scope, :message)';
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':user_id' => $user_id,
|
||||
':scope' => $scope,
|
||||
':message' => $message,
|
||||
]);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* PSR-3 compatible log method
|
||||
* @param string $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* Retrieve log entries from the database.
|
||||
*
|
||||
* @param int $user_id 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.
|
||||
*
|
||||
* @return array An array of log entries.
|
||||
*/
|
||||
public function log(string $level, string $message, array $context = []): void {
|
||||
$this->logger->log($level, $message, $context);
|
||||
public function readLog($user_id, $scope, $offset=0, $items_per_page='') {
|
||||
if ($scope === 'user') {
|
||||
$sql = 'SELECT * FROM logs WHERE user_id = :user_id ORDER BY time DESC';
|
||||
if ($items_per_page) {
|
||||
$items_per_page = (int)$items_per_page;
|
||||
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
|
||||
}
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':user_id' => $user_id,
|
||||
]);
|
||||
}
|
||||
if ($scope === 'system') {
|
||||
$sql = 'SELECT * FROM logs ORDER BY time DESC';
|
||||
if ($items_per_page) {
|
||||
$items_per_page = (int)$items_per_page;
|
||||
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
|
||||
}
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<?php
|
||||
|
||||
class Feedback {
|
||||
// Feedback types
|
||||
class Messages {
|
||||
// Message types
|
||||
const TYPE_SUCCESS = 'success';
|
||||
const TYPE_ERROR = 'danger';
|
||||
const TYPE_INFO = 'info';
|
||||
const TYPE_WARNING = 'warning';
|
||||
|
||||
// Default feedback message configurations
|
||||
// Default message configurations
|
||||
const NOTICE = [
|
||||
'DEFAULT' => [
|
||||
'type' => self::TYPE_INFO,
|
||||
|
@ -35,10 +35,6 @@ class Feedback {
|
|||
'type' => self::TYPE_SUCCESS,
|
||||
'dismissible' => true
|
||||
],
|
||||
'SESSION_TIMEOUT' => [
|
||||
'type' => self::TYPE_ERROR,
|
||||
'dismissible' => true
|
||||
],
|
||||
'IP_BLACKLISTED' => [
|
||||
'type' => self::TYPE_ERROR,
|
||||
'dismissible' => false
|
||||
|
@ -53,21 +49,6 @@ class Feedback {
|
|||
]
|
||||
];
|
||||
|
||||
const REGISTER = [
|
||||
'SUCCESS' => [
|
||||
'type' => self::TYPE_SUCCESS,
|
||||
'dismissible' => true
|
||||
],
|
||||
'FAILED' => [
|
||||
'type' => self::TYPE_ERROR,
|
||||
'dismissible' => true
|
||||
],
|
||||
'DISABLED' => [
|
||||
'type' => self::TYPE_ERROR,
|
||||
'dismissible' => false
|
||||
],
|
||||
];
|
||||
|
||||
const SECURITY = [
|
||||
'WHITELIST_ADD_SUCCESS' => [
|
||||
'type' => self::TYPE_SUCCESS,
|
||||
|
@ -115,46 +96,20 @@ class Feedback {
|
|||
]
|
||||
];
|
||||
|
||||
const THEME = [
|
||||
'THEME_CHANGE_SUCCESS' => [
|
||||
'type' => self::TYPE_SUCCESS,
|
||||
'dismissible' => true
|
||||
],
|
||||
'THEME_CHANGE_FAILED' => [
|
||||
'type' => self::TYPE_ERROR,
|
||||
'dismissible' => true
|
||||
]
|
||||
];
|
||||
|
||||
const SYSTEM = [
|
||||
'DB_ERROR' => [
|
||||
'type' => self::TYPE_ERROR,
|
||||
'dismissible' => false
|
||||
],
|
||||
'DB_CONNECT_ERROR' => [
|
||||
'type' => self::TYPE_ERROR,
|
||||
'dismissible' => false
|
||||
],
|
||||
'DB_UNKNOWN_TYPE' => [
|
||||
'type' => self::TYPE_ERROR,
|
||||
'dismissible' => false
|
||||
],
|
||||
];
|
||||
|
||||
private static $strings = null;
|
||||
|
||||
/**
|
||||
* Get feedback message strings
|
||||
* Get message strings
|
||||
*/
|
||||
private static function getStrings() {
|
||||
if (self::$strings === null) {
|
||||
self::$strings = require __DIR__ . '/../includes/strings.php';
|
||||
self::$strings = require __DIR__ . '/../includes/messages-strings.php';
|
||||
}
|
||||
return self::$strings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feedback message configuration by key
|
||||
* Get message configuration by key
|
||||
*/
|
||||
public static function get($category, $key) {
|
||||
$config = constant("self::$category")[$key] ?? null;
|
||||
|
@ -167,9 +122,9 @@ class Feedback {
|
|||
}
|
||||
|
||||
/**
|
||||
* Render feedback message HTML
|
||||
* Render message HTML
|
||||
*/
|
||||
// Usage: echo Feedback::render('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
|
||||
// Usage: echo Messages::render('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
|
||||
public static function render($category, $key, $customMessage = null, $dismissible = null, $small = false, $sanitize = true) {
|
||||
$config = self::get($category, $key);
|
||||
if (!$config) return '';
|
||||
|
@ -191,30 +146,15 @@ class Feedback {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get feedback message data for JavaScript
|
||||
* Store message in session for display after redirect
|
||||
*/
|
||||
public static function getMessageData($category, $key, $customMessage = null, $dismissible = null, $small = false) {
|
||||
$config = self::get($category, $key);
|
||||
if (!$config) return null;
|
||||
|
||||
return [
|
||||
'type' => $config['type'],
|
||||
'message' => $customMessage ?? $config['message'],
|
||||
'dismissible' => $dismissible ?? $config['dismissible'] ?? false,
|
||||
'small' => $small
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Store feedback message in session for display after redirect
|
||||
*/
|
||||
// Usage: Feedback::flash('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
|
||||
// Usage: Messages::flash('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
|
||||
public static function flash($category, $key, $customMessage = null, $dismissible = null, $small = false) {
|
||||
if (!isset($_SESSION['flash_messages'])) {
|
||||
$_SESSION['flash_messages'] = [];
|
||||
}
|
||||
|
||||
// Get the feedback message configuration
|
||||
// Get the message configuration
|
||||
$config = self::get($category, $key);
|
||||
$isDismissible = $dismissible ?? $config['dismissible'] ?? false;
|
||||
|
||||
|
@ -228,11 +168,11 @@ class Feedback {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get and clear all flash feedback messages
|
||||
* Get and clear all flash messages
|
||||
*/
|
||||
public static function getFlash() {
|
||||
$system_messages = $_SESSION['flash_messages'] ?? [];
|
||||
$messages = $_SESSION['flash_messages'] ?? [];
|
||||
unset($_SESSION['flash_messages']);
|
||||
return $system_messages;
|
||||
return $messages;
|
||||
}
|
||||
}
|
|
@ -372,3 +372,5 @@ AND pe.event_type = 'participant joining'";
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Handles password reset functionality including token generation and validation
|
||||
*/
|
||||
class PasswordReset {
|
||||
private $db;
|
||||
private const TOKEN_LENGTH = 32;
|
||||
private const TOKEN_EXPIRY = 3600; // 1 hour
|
||||
|
||||
public function __construct($database) {
|
||||
if ($database instanceof PDO) {
|
||||
$this->db = $database;
|
||||
} else {
|
||||
$this->db = $database->getConnection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a password reset request and sends email to user
|
||||
*
|
||||
* @param string $email User's email address
|
||||
* @return array Status of the reset request
|
||||
*/
|
||||
public function requestReset($email) {
|
||||
// Check if email exists
|
||||
$query = $this->db->prepare("
|
||||
SELECT u.id, um.email
|
||||
FROM user u
|
||||
JOIN user_meta um ON u.id = um.user_id
|
||||
WHERE um.email = :email"
|
||||
);
|
||||
$query->bindParam(':email', $email);
|
||||
$query->execute();
|
||||
|
||||
$user = $query->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$user) {
|
||||
return ['success' => false, 'message' => 'If this email exists in our system, you will receive reset instructions.'];
|
||||
}
|
||||
|
||||
// Generate unique token
|
||||
$token = bin2hex(random_bytes(self::TOKEN_LENGTH / 2));
|
||||
$expires = time() + self::TOKEN_EXPIRY;
|
||||
|
||||
// Store token in database
|
||||
$query = $this->db->prepare("
|
||||
INSERT INTO user_password_reset (user_id, token, expires)
|
||||
VALUES (:user_id, :token, :expires)"
|
||||
);
|
||||
$query->bindParam(':user_id', $user['id']);
|
||||
$query->bindParam(':token', $token);
|
||||
$query->bindParam(':expires', $expires);
|
||||
|
||||
if (!$query->execute()) {
|
||||
return ['success' => false, 'message' => 'Failed to process reset request'];
|
||||
}
|
||||
|
||||
// We need the config for the email details
|
||||
global $config;
|
||||
|
||||
// Prepare the reset link
|
||||
$scheme = $_SERVER['REQUEST_SCHEME'];
|
||||
$domain = trim($config['domain'], '/');
|
||||
$folder = trim($config['folder'], '/');
|
||||
$folderPath = $folder !== '' ? "/$folder" : '';
|
||||
$resetLink = "{$scheme}://{$domain}{$folderPath}/index.php?page=login&action=reset&token=" . urlencode($token);
|
||||
|
||||
// Send email with reset link
|
||||
$to = $user['email'];
|
||||
$subject = "{$config['site_name']} - Password reset request";
|
||||
$message = "Dear user,\n\n";
|
||||
$message .= "We received a request to reset your password for your {$config['site_name']} account.\n\n";
|
||||
$message .= "To set a new password, please click the link below:\n\n";
|
||||
$message .= $resetLink . "\n\n";
|
||||
$message .= "This link will expire in 1 hour for security reasons.\n\n";
|
||||
$message .= "If you did not request this password reset, please ignore this email. Your account remains secure.\n\n";
|
||||
if (!empty($config['site_name'])) {
|
||||
$message .= "Best regards,\n";
|
||||
$message .= "The {$config['site_name']} team\n";
|
||||
if (!empty($config['site_slogan'])) {
|
||||
$message .= ":: {$config['site_slogan']} ::";
|
||||
}
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'From' => "noreply@{$config['domain']}",
|
||||
'Reply-To' => "noreply@{$config['domain']}",
|
||||
'X-Mailer' => 'PHP/' . phpversion()
|
||||
];
|
||||
|
||||
if (!mail($to, $subject, $message, $headers)) {
|
||||
return ['success' => false, 'message' => 'Failed to send reset email'];
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => 'If this email exists in our system, you will receive reset instructions.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a reset token and returns associated user ID if valid
|
||||
*
|
||||
* @param string $token Reset token
|
||||
* @return array Validation result with user ID if successful
|
||||
*/
|
||||
public function validateToken($token) {
|
||||
$now = time();
|
||||
|
||||
$query = $this->db->prepare("
|
||||
SELECT user_id
|
||||
FROM user_password_reset
|
||||
WHERE token = :token
|
||||
AND expires > :now
|
||||
AND used = 0
|
||||
");
|
||||
|
||||
$query->bindParam(':token', $token);
|
||||
$query->bindParam(':now', $now);
|
||||
$query->execute();
|
||||
|
||||
$result = $query->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$result) {
|
||||
return ['valid' => false];
|
||||
}
|
||||
|
||||
return ['valid' => true, 'user_id' => $result['user_id']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the password reset process
|
||||
*
|
||||
* @param string $token Reset token
|
||||
* @param string $newPassword New password
|
||||
* @return bool Whether reset was successful
|
||||
*/
|
||||
public function resetPassword($token, $newPassword) {
|
||||
$validation = $this->validateToken($token);
|
||||
if (!$validation['valid']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start transaction
|
||||
$this->db->beginTransaction();
|
||||
|
||||
try {
|
||||
// Update password
|
||||
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
$query = $this->db->prepare(
|
||||
"UPDATE user
|
||||
SET password = :password
|
||||
WHERE id = :user_id"
|
||||
);
|
||||
$query->bindParam(':password', $hashedPassword);
|
||||
$query->bindParam(':user_id', $validation['user_id']);
|
||||
$query->execute();
|
||||
|
||||
// Mark token as used
|
||||
$query = $this->db->prepare(
|
||||
"UPDATE user_password_reset
|
||||
SET used = 1
|
||||
WHERE token = :token"
|
||||
);
|
||||
$query->bindParam(':token', $token);
|
||||
$query->execute();
|
||||
|
||||
$this->db->commit();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->db->rollBack();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ class Platform {
|
|||
* @return array An associative array containing platform details.
|
||||
*/
|
||||
public function getPlatformDetails($platform_id = '') {
|
||||
$sql = 'SELECT * FROM platform';
|
||||
$sql = 'SELECT * FROM platforms';
|
||||
if ($platform_id !== '') {
|
||||
$sql .= ' WHERE id = :platform_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
|
@ -57,7 +57,7 @@ class Platform {
|
|||
*/
|
||||
public function addPlatform($newPlatform) {
|
||||
try {
|
||||
$sql = 'INSERT INTO platform
|
||||
$sql = 'INSERT INTO platforms
|
||||
(name, jitsi_url, jilo_database)
|
||||
VALUES
|
||||
(:name, :jitsi_url, :jilo_database)';
|
||||
|
@ -90,7 +90,7 @@ class Platform {
|
|||
*/
|
||||
public function editPlatform($platform_id, $updatedPlatform) {
|
||||
try {
|
||||
$sql = 'UPDATE platform SET
|
||||
$sql = 'UPDATE platforms SET
|
||||
name = :name,
|
||||
jitsi_url = :jitsi_url,
|
||||
jilo_database = :jilo_database
|
||||
|
@ -122,42 +122,21 @@ class Platform {
|
|||
*/
|
||||
public function deletePlatform($platform_id) {
|
||||
try {
|
||||
$this->db->beginTransaction();
|
||||
$sql = 'DELETE FROM platforms
|
||||
WHERE
|
||||
id = :platform_id';
|
||||
|
||||
// First, get all hosts in this platform
|
||||
$sql = 'SELECT id FROM host WHERE platform_id = :platform_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->bindParam(':platform_id', $platform_id);
|
||||
$query->execute();
|
||||
$hosts = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Delete all agents for each host
|
||||
foreach ($hosts as $host) {
|
||||
$sql = 'DELETE FROM jilo_agent WHERE host_id = :host_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->bindParam(':host_id', $host['id']);
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
// Delete all hosts in this platform
|
||||
$sql = 'DELETE FROM host WHERE platform_id = :platform_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->bindParam(':platform_id', $platform_id);
|
||||
$query->execute();
|
||||
|
||||
// Finally, delete the platform
|
||||
$sql = 'DELETE FROM platform WHERE id = :platform_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->bindParam(':platform_id', $platform_id);
|
||||
$query->execute();
|
||||
|
||||
$this->db->commit();
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->db->rollBack();
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,96 +1,53 @@
|
|||
<?php
|
||||
|
||||
use App\Core\NullLogger;
|
||||
|
||||
class RateLimiter {
|
||||
public $db;
|
||||
private $database;
|
||||
/** @var mixed NullLogger (or PSR-3 logger) or plugin Log */
|
||||
private $logger;
|
||||
private $log;
|
||||
public $maxAttempts = 5; // Maximum login attempts
|
||||
public $decayMinutes = 15; // Time window in minutes
|
||||
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
|
||||
public $autoBlacklistDuration = 24; // Hours to blacklist for
|
||||
public $authRatelimitTable = 'security_rate_auth'; // For rate limiting username/password attempts
|
||||
public $pagesRatelimitTable = 'security_rate_page'; // For rate limiting page requests
|
||||
public $whitelistTable = 'security_ip_whitelist'; // For whitelisting IPs and network ranges
|
||||
public $blacklistTable = 'security_ip_blacklist'; // For blacklisting IPs and network ranges
|
||||
private $pageLimits = [
|
||||
// Default rate limits per minute
|
||||
'default' => 60,
|
||||
'admin' => 120,
|
||||
'message' => 20,
|
||||
'contact' => 30,
|
||||
'call' => 30,
|
||||
'register' => 5,
|
||||
'config' => 10
|
||||
];
|
||||
public $ratelimitTable = 'login_attempts';
|
||||
public $whitelistTable = 'ip_whitelist';
|
||||
public $blacklistTable = 'ip_blacklist';
|
||||
|
||||
/**
|
||||
* @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;
|
||||
public function __construct($database) {
|
||||
$this->db = $database->getConnection();
|
||||
// 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->log = new Log($database);
|
||||
$this->createTablesIfNotExist();
|
||||
}
|
||||
|
||||
// Database preparation
|
||||
private function createTablesIfNotExist() {
|
||||
// Authentication attempts table
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
|
||||
id int(11) 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->exec($sql);
|
||||
|
||||
// Pages rate limits table
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->pagesRatelimitTable} (
|
||||
id int(11) 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)
|
||||
// Login attempts table
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->ratelimitTable} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_address TEXT NOT NULL UNIQUE,
|
||||
username TEXT NOT NULL,
|
||||
attempted_at TEXT DEFAULT (DATETIME('now'))
|
||||
)";
|
||||
$this->db->exec($sql);
|
||||
|
||||
// IP whitelist table
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
|
||||
id int(11) 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)
|
||||
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->exec($sql);
|
||||
|
||||
// IP blacklist table
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} (
|
||||
id int(11) 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)
|
||||
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->db->exec($sql);
|
||||
|
||||
|
@ -104,7 +61,7 @@ class RateLimiter {
|
|||
];
|
||||
|
||||
// Insert default whitelisted IPs if they don't exist
|
||||
$stmt = $this->db->prepare("INSERT IGNORE INTO {$this->whitelistTable}
|
||||
$stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->whitelistTable}
|
||||
(ip_address, is_network, description, created_by)
|
||||
VALUES (?, ?, ?, 'system')");
|
||||
foreach ($defaultIps as $ip) {
|
||||
|
@ -120,7 +77,7 @@ class RateLimiter {
|
|||
['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737']
|
||||
];
|
||||
|
||||
$stmt = $this->db->prepare("INSERT IGNORE INTO {$this->blacklistTable}
|
||||
$stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->blacklistTable}
|
||||
(ip_address, is_network, reason, created_by)
|
||||
VALUES (?, ?, ?, 'system')");
|
||||
|
||||
|
@ -134,8 +91,8 @@ class RateLimiter {
|
|||
* Get number of recent login attempts for an IP
|
||||
*/
|
||||
public function getRecentAttempts($ip) {
|
||||
$stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->authRatelimitTable}
|
||||
WHERE ip_address = ? AND attempted_at > DATE_SUB(NOW(), INTERVAL ? MINUTE)");
|
||||
$stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->ratelimitTable}
|
||||
WHERE ip_address = ? AND attempted_at > datetime('now', '-' || :minutes || ' minutes')");
|
||||
$stmt->execute([$ip, $this->decayMinutes]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return intval($result['attempts']);
|
||||
|
@ -146,20 +103,7 @@ class RateLimiter {
|
|||
*/
|
||||
public function isIpBlacklisted($ip) {
|
||||
// First check if IP is explicitly blacklisted or in a blacklisted range
|
||||
$stmt = $this->db->prepare("SELECT ip_address, is_network, expiry_time FROM {$this->blacklistTable} WHERE ip_address = ?");
|
||||
$stmt->execute([$ip]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row) {
|
||||
// Skip expired entries
|
||||
if ($row['expiry_time'] !== null && strtotime($row['expiry_time']) < time()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check network ranges
|
||||
$stmt = $this->db->prepare("SELECT ip_address, expiry_time FROM {$this->blacklistTable} WHERE is_network = 1");
|
||||
$stmt = $this->db->prepare("SELECT ip_address, is_network, expiry_time FROM {$this->blacklistTable}");
|
||||
$stmt->execute();
|
||||
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
|
@ -168,9 +112,15 @@ class RateLimiter {
|
|||
continue;
|
||||
}
|
||||
|
||||
if ($row['is_network']) {
|
||||
if ($this->ipInRange($ip, $row['ip_address'])) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if ($ip === $row['ip_address']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -180,24 +130,19 @@ class RateLimiter {
|
|||
* Check if an IP is whitelisted
|
||||
*/
|
||||
public function isIpWhitelisted($ip) {
|
||||
// Check exact IP match first
|
||||
$stmt = $this->db->prepare("SELECT ip_address FROM {$this->whitelistTable} WHERE ip_address = ?");
|
||||
$stmt->execute([$ip]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only check ranges for IPv4 addresses
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
// Check network ranges
|
||||
$stmt = $this->db->prepare("SELECT ip_address FROM {$this->whitelistTable} WHERE is_network = 1");
|
||||
// Check exact IP match and CIDR ranges
|
||||
$stmt = $this->db->prepare("SELECT ip_address, is_network FROM {$this->whitelistTable}");
|
||||
$stmt->execute();
|
||||
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
if ($row['is_network']) {
|
||||
if ($this->ipInRange($ip, $row['ip_address'])) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if ($ip === $row['ip_address']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,18 +150,8 @@ class RateLimiter {
|
|||
}
|
||||
|
||||
private function ipInRange($ip, $cidr) {
|
||||
// Only work with IPv4 addresses
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list($subnet, $bits) = explode('/', $cidr);
|
||||
|
||||
// Make sure subnet is IPv4
|
||||
if (!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ip = ip2long($ip);
|
||||
$subnet = ip2long($subnet);
|
||||
$mask = -1 << (32 - $bits);
|
||||
|
@ -232,19 +167,15 @@ class RateLimiter {
|
|||
if ($this->isIpBlacklisted($ip)) {
|
||||
$message = "Cannot whitelist {$ip} - IP is currently blacklisted";
|
||||
if ($userId) {
|
||||
$this->logger->log('info', "IP Whitelist: {$message}", ['user_id' => $userId, 'scope' => 'system']);
|
||||
Feedback::flash('ERROR', 'DEFAULT', $message);
|
||||
$this->log->insertLog($userId, "IP Whitelist: {$message}", 'system');
|
||||
Messages::flash('ERROR', 'DEFAULT', $message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare("INSERT INTO {$this->whitelistTable}
|
||||
$stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->whitelistTable}
|
||||
(ip_address, is_network, description, created_by)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
is_network = VALUES(is_network),
|
||||
description = VALUES(description),
|
||||
created_by = VALUES(created_by)");
|
||||
VALUES (?, ?, ?, ?)");
|
||||
|
||||
$result = $stmt->execute([$ip, $isNetwork, $description, $createdBy]);
|
||||
|
||||
|
@ -256,15 +187,15 @@ class RateLimiter {
|
|||
$createdBy,
|
||||
$description
|
||||
);
|
||||
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
|
||||
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($userId) {
|
||||
$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());
|
||||
$this->log->insertLog($userId, "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), 'system');
|
||||
Messages::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -291,15 +222,15 @@ class RateLimiter {
|
|||
$removedBy,
|
||||
$ipDetails['created_by']
|
||||
);
|
||||
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
|
||||
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($userId) {
|
||||
$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());
|
||||
$this->log->insertLog($userId, "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), 'system');
|
||||
Messages::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -311,22 +242,17 @@ class RateLimiter {
|
|||
if ($this->isIpWhitelisted($ip)) {
|
||||
$message = "Cannot blacklist {$ip} - IP is currently whitelisted";
|
||||
if ($userId) {
|
||||
$this->logger->log('info', "IP Blacklist: {$message}", ['user_id' => $userId, 'scope' => 'system']);
|
||||
Feedback::flash('ERROR', 'DEFAULT', $message);
|
||||
$this->log->insertLog($userId, "IP Blacklist: {$message}", 'system');
|
||||
Messages::flash('ERROR', 'DEFAULT', $message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null;
|
||||
|
||||
$stmt = $this->db->prepare("INSERT INTO {$this->blacklistTable}
|
||||
$stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->blacklistTable}
|
||||
(ip_address, is_network, reason, expiry_time, created_by)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
is_network = VALUES(is_network),
|
||||
reason = VALUES(reason),
|
||||
expiry_time = VALUES(expiry_time),
|
||||
created_by = VALUES(created_by)");
|
||||
VALUES (?, ?, ?, ?, ?)");
|
||||
|
||||
$result = $stmt->execute([$ip, $isNetwork, $reason, $expiryTime, $createdBy]);
|
||||
|
||||
|
@ -339,14 +265,14 @@ class RateLimiter {
|
|||
$reason,
|
||||
$expiryTime ?? 'never'
|
||||
);
|
||||
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
|
||||
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (Exception $e) {
|
||||
if ($userId) {
|
||||
$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());
|
||||
$this->log->insertLog($userId, "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), 'system');
|
||||
Messages::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -361,7 +287,6 @@ class RateLimiter {
|
|||
|
||||
// Remove the IP
|
||||
$stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable} WHERE ip_address = ?");
|
||||
|
||||
$result = $stmt->execute([$ip]);
|
||||
|
||||
if ($result && $ipDetails) {
|
||||
|
@ -373,14 +298,14 @@ class RateLimiter {
|
|||
$ipDetails['created_by'],
|
||||
$ipDetails['reason']
|
||||
);
|
||||
$this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
|
||||
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (Exception $e) {
|
||||
if ($userId) {
|
||||
$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());
|
||||
$this->log->insertLog($userId, "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), 'system');
|
||||
Messages::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -404,18 +329,18 @@ class RateLimiter {
|
|||
try {
|
||||
// Remove expired blacklist entries
|
||||
$stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable}
|
||||
WHERE expiry_time IS NOT NULL AND expiry_time < NOW()");
|
||||
WHERE expiry_time IS NOT NULL AND expiry_time < datetime('now')");
|
||||
$stmt->execute();
|
||||
|
||||
// Clean old login attempts
|
||||
$stmt = $this->db->prepare("DELETE FROM {$this->authRatelimitTable}
|
||||
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)");
|
||||
$stmt = $this->db->prepare("DELETE FROM {$this->ratelimitTable}
|
||||
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')");
|
||||
$stmt->execute([':minutes' => $this->decayMinutes]);
|
||||
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$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());
|
||||
$this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system');
|
||||
Messages::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -441,9 +366,9 @@ class RateLimiter {
|
|||
|
||||
// Check total attempts across all usernames from this IP
|
||||
$sql = "SELECT COUNT(*) as total_attempts
|
||||
FROM {$this->authRatelimitTable}
|
||||
FROM {$this->ratelimitTable}
|
||||
WHERE ip_address = :ip
|
||||
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
|
||||
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':ip' => $ipAddress,
|
||||
|
@ -455,21 +380,25 @@ class RateLimiter {
|
|||
return $result['total_attempts'] < $this->autoBlacklistThreshold;
|
||||
}
|
||||
|
||||
public function attempt($username, $ipAddress, $failed = true) {
|
||||
// Only record failed attempts
|
||||
if (!$failed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function attempt($username, $ipAddress) {
|
||||
// Record this attempt
|
||||
$sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)";
|
||||
$sql = "INSERT INTO {$this->ratelimitTable} (ip_address, username) VALUES (:ip, :username)";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
try {
|
||||
$stmt->execute([
|
||||
':ip' => $ipAddress,
|
||||
':username' => $username
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
|
||||
// Auto-blacklist if too many attempts
|
||||
if (!$this->isAllowed($username, $ipAddress)) {
|
||||
$this->addToBlacklist(
|
||||
$ipAddress,
|
||||
false,
|
||||
'Auto-blacklisted due to excessive login attempts',
|
||||
'system',
|
||||
null,
|
||||
$this->autoBlacklistDuration
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -478,10 +407,10 @@ class RateLimiter {
|
|||
|
||||
public function tooManyAttempts($username, $ipAddress) {
|
||||
$sql = "SELECT COUNT(*) as attempts
|
||||
FROM {$this->authRatelimitTable}
|
||||
FROM {$this->ratelimitTable}
|
||||
WHERE ip_address = :ip
|
||||
AND username = :username
|
||||
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
|
||||
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
|
@ -491,33 +420,12 @@ class RateLimiter {
|
|||
]);
|
||||
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Also check what's in the table
|
||||
$sql = "SELECT * FROM {$this->authRatelimitTable} WHERE ip_address = :ip";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([':ip' => $ipAddress]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$tooMany = $result['attempts'] >= $this->maxAttempts;
|
||||
|
||||
// Auto-blacklist if too many attempts
|
||||
if ($tooMany) {
|
||||
$this->addToBlacklist(
|
||||
$ipAddress,
|
||||
false,
|
||||
'Auto-blacklisted due to excessive login attempts',
|
||||
'system',
|
||||
null,
|
||||
$this->autoBlacklistDuration
|
||||
);
|
||||
}
|
||||
|
||||
return $tooMany;
|
||||
return $result['attempts'] >= $this->maxAttempts;
|
||||
}
|
||||
|
||||
public function clearOldAttempts() {
|
||||
$sql = "DELETE FROM {$this->authRatelimitTable}
|
||||
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
|
||||
$sql = "DELETE FROM {$this->ratelimitTable}
|
||||
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
|
@ -527,10 +435,10 @@ class RateLimiter {
|
|||
|
||||
public function getRemainingAttempts($username, $ipAddress) {
|
||||
$sql = "SELECT COUNT(*) as attempts
|
||||
FROM {$this->authRatelimitTable}
|
||||
FROM {$this->ratelimitTable}
|
||||
WHERE ip_address = :ip
|
||||
AND username = :username
|
||||
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
|
||||
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
|
@ -546,123 +454,4 @@ class RateLimiter {
|
|||
public function getDecayMinutes() {
|
||||
return $this->decayMinutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page request is allowed
|
||||
*/
|
||||
public function isPageRequestAllowed($ipAddress, $endpoint, $userId = null) {
|
||||
// First check if IP is blacklisted
|
||||
if ($this->isIpBlacklisted($ipAddress)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Then check if IP is whitelisted
|
||||
if ($this->isIpWhitelisted($ipAddress)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clean old requests
|
||||
$this->cleanOldPageRequests();
|
||||
|
||||
// Get limit based on endpoint type and user role
|
||||
$limit = $this->getPageLimitForEndpoint($endpoint, $userId);
|
||||
|
||||
// Count recent requests, including this one
|
||||
$sql = "SELECT COUNT(*) as request_count
|
||||
FROM {$this->pagesRatelimitTable}
|
||||
WHERE ip_address = :ip
|
||||
AND endpoint = :endpoint
|
||||
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':ip' => $ipAddress,
|
||||
':endpoint' => $endpoint
|
||||
]);
|
||||
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $result['request_count'] < $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a page request
|
||||
*/
|
||||
public function recordPageRequest($ipAddress, $endpoint) {
|
||||
$sql = "INSERT INTO {$this->pagesRatelimitTable} (ip_address, endpoint)
|
||||
VALUES (:ip, :endpoint)";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
return $stmt->execute([
|
||||
':ip' => $ipAddress,
|
||||
':endpoint' => $endpoint
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean old page requests
|
||||
*/
|
||||
private function cleanOldPageRequests() {
|
||||
$sql = "DELETE FROM {$this->pagesRatelimitTable}
|
||||
WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page rate limit for endpoint
|
||||
*/
|
||||
private function getPageLimitForEndpoint($endpoint, $userId = null) {
|
||||
// Admin users get higher limits
|
||||
if ($userId) {
|
||||
// Check admin rights directly from database
|
||||
$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'];
|
||||
}
|
||||
}
|
||||
|
||||
// Get endpoint type from the endpoint path
|
||||
$endpointType = $this->getEndpointType($endpoint);
|
||||
|
||||
// Return specific limit if exists, otherwise default
|
||||
return isset($this->pageLimits[$endpointType])
|
||||
? $this->pageLimits[$endpointType]
|
||||
: $this->pageLimits['default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get endpoint type from path
|
||||
*/
|
||||
private function getEndpointType($endpoint) {
|
||||
if (strpos($endpoint, 'message') !== false) return 'message';
|
||||
if (strpos($endpoint, 'contact') !== false) return 'contact';
|
||||
if (strpos($endpoint, 'call') !== false) return 'call';
|
||||
if (strpos($endpoint, 'register') !== false) return 'register';
|
||||
if (strpos($endpoint, 'config') !== false) return 'config';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining page requests
|
||||
*/
|
||||
public function getRemainingPageRequests($ipAddress, $endpoint, $userId = null) {
|
||||
$limit = $this->getPageLimitForEndpoint($endpoint, $userId);
|
||||
|
||||
$sql = "SELECT COUNT(*) as request_count
|
||||
FROM {$this->pagesRatelimitTable}
|
||||
WHERE ip_address = :ip
|
||||
AND endpoint = :endpoint
|
||||
AND request_time > DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':ip' => $ipAddress,
|
||||
':endpoint' => $endpoint
|
||||
]);
|
||||
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return max(0, $limit - $result['request_count']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,3 +83,5 @@ class Router {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -52,3 +52,5 @@ class Server {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,265 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Session Class
|
||||
*
|
||||
* Core session management functionality for the application
|
||||
*/
|
||||
class Session {
|
||||
private static $initialized = false;
|
||||
private static $sessionName = ''; // Will be set from config, if not we'll have a random session name
|
||||
|
||||
/**
|
||||
* Generate a random session name
|
||||
*/
|
||||
private static function generateRandomSessionName(): string {
|
||||
return 'sess_' . bin2hex(random_bytes(8)); // 16-character random string
|
||||
}
|
||||
private static $sessionOptions = [
|
||||
'cookie_httponly' => 1,
|
||||
'cookie_secure' => 1,
|
||||
'cookie_samesite' => 'Strict',
|
||||
'gc_maxlifetime' => 7200 // 2 hours
|
||||
];
|
||||
|
||||
/**
|
||||
* Initialize session configuration
|
||||
*/
|
||||
private static function initialize() {
|
||||
if (self::$initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $config;
|
||||
|
||||
// Get session name from config or generate a random one
|
||||
self::$sessionName = $config['session']['name'] ?? self::generateRandomSessionName();
|
||||
|
||||
// Set session name before starting the session
|
||||
session_name(self::$sessionName);
|
||||
|
||||
// Set session cookie parameters
|
||||
$thisPath = $config['folder'] ?? '/';
|
||||
$thisDomain = $config['domain'] ?? '';
|
||||
$isSecure = isset($_SERVER['HTTPS']);
|
||||
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0, // Session cookie (browser session)
|
||||
'path' => $thisPath,
|
||||
'domain' => $thisDomain,
|
||||
'secure' => $isSecure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
]);
|
||||
|
||||
self::$initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session name from config or generate a random one
|
||||
*/
|
||||
private static function getSessionNameFromConfig($config) {
|
||||
if (isset($config['session']['name']) && !empty($config['session']['name'])) {
|
||||
return $config['session']['name'];
|
||||
}
|
||||
return self::generateRandomSessionName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start or resume a session with secure options
|
||||
*/
|
||||
public static function startSession() {
|
||||
self::initialize();
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
if (!headers_sent()) {
|
||||
session_start(self::$sessionOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy current session and clean up
|
||||
*/
|
||||
public static function destroySession() {
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current username if set
|
||||
*/
|
||||
public static function getUsername() {
|
||||
return isset($_SESSION['username']) ? htmlspecialchars($_SESSION['username']) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user ID if set
|
||||
*/
|
||||
public static function getUserId() {
|
||||
return isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current session is valid
|
||||
*
|
||||
* @param bool $strict If true, will return false for new/unauthenticated sessions
|
||||
* @return bool True if session is valid, false otherwise
|
||||
*/
|
||||
public static function isValidSession($strict = true) {
|
||||
// If session is not started or empty, it's not valid
|
||||
if (session_status() !== PHP_SESSION_ACTIVE || empty($_SESSION)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// In non-strict mode, consider empty session as valid (for login/logout)
|
||||
if (!$strict && !isset($_SESSION['user_id']) && !isset($_SESSION['username'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// In strict mode, require user_id and username
|
||||
if ($strict && (!isset($_SESSION['user_id']) || !isset($_SESSION['username']))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check session timeout
|
||||
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 7200; // 30 days or 2 hours
|
||||
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update last activity time
|
||||
$_SESSION['LAST_ACTIVITY'] = time();
|
||||
|
||||
// Regenerate session ID periodically (every 30 minutes)
|
||||
if (!isset($_SESSION['CREATED'])) {
|
||||
$_SESSION['CREATED'] = time();
|
||||
} else if (time() - $_SESSION['CREATED'] > 1800) {
|
||||
// Regenerate session ID and update creation time
|
||||
if (!headers_sent() && session_status() === PHP_SESSION_ACTIVE) {
|
||||
$oldData = $_SESSION;
|
||||
session_regenerate_id(true);
|
||||
$_SESSION = $oldData;
|
||||
$_SESSION['CREATED'] = time();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set remember me option for extended session
|
||||
*/
|
||||
public static function setRememberMe($value = true) {
|
||||
$_SESSION['REMEMBER_ME'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear session data and cookies
|
||||
*/
|
||||
public static function cleanup($config) {
|
||||
self::destroySession();
|
||||
|
||||
// Clear cookies if headers not sent
|
||||
if (!headers_sent()) {
|
||||
setcookie('username', '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => $config['folder'],
|
||||
'domain' => $config['domain'],
|
||||
'secure' => isset($_SERVER['HTTPS']),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
]);
|
||||
}
|
||||
|
||||
// Start fresh session
|
||||
self::startSession();
|
||||
|
||||
// Reset session timeout flag
|
||||
unset($_SESSION['session_timeout_shown']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new authenticated session for a user
|
||||
*/
|
||||
public static function createAuthSession($userId, $username, $rememberMe, $config) {
|
||||
// Ensure session is started
|
||||
self::startSession();
|
||||
|
||||
// Set session variables
|
||||
$_SESSION['user_id'] = $userId;
|
||||
$_SESSION['username'] = $username;
|
||||
$_SESSION['LAST_ACTIVITY'] = time();
|
||||
$_SESSION['REMEMBER_ME'] = $rememberMe;
|
||||
|
||||
// Set cookie lifetime based on remember me
|
||||
$cookieLifetime = $rememberMe ? time() + (30 * 24 * 60 * 60) : 0;
|
||||
|
||||
// Update session cookie with remember me setting
|
||||
if (!headers_sent()) {
|
||||
setcookie(
|
||||
session_name(),
|
||||
session_id(),
|
||||
[
|
||||
'expires' => $cookieLifetime,
|
||||
'path' => $config['folder'] ?? '/',
|
||||
'domain' => $config['domain'] ?? '',
|
||||
'secure' => isset($_SERVER['HTTPS']),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
]
|
||||
);
|
||||
|
||||
// Set username cookie
|
||||
setcookie('username', $username, [
|
||||
'expires' => $cookieLifetime,
|
||||
'path' => $config['folder'] ?? '/',
|
||||
'domain' => $config['domain'] ?? '',
|
||||
'secure' => isset($_SERVER['HTTPS']),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
]);
|
||||
}
|
||||
|
||||
if ($rememberMe) {
|
||||
self::setRememberMe(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store 2FA pending information in session
|
||||
*/
|
||||
public static function store2FAPending($userId, $username, $rememberMe = false) {
|
||||
$_SESSION['2fa_pending_user_id'] = $userId;
|
||||
$_SESSION['2fa_pending_username'] = $username;
|
||||
if ($rememberMe) {
|
||||
$_SESSION['2fa_pending_remember'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear 2FA pending information from session
|
||||
*/
|
||||
public static function clear2FAPending() {
|
||||
unset($_SESSION['2fa_pending_user_id']);
|
||||
unset($_SESSION['2fa_pending_username']);
|
||||
unset($_SESSION['2fa_pending_remember']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 2FA pending information
|
||||
*/
|
||||
public static function get2FAPending() {
|
||||
if (!isset($_SESSION['2fa_pending_user_id']) || !isset($_SESSION['2fa_pending_username'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'user_id' => $_SESSION['2fa_pending_user_id'],
|
||||
'username' => $_SESSION['2fa_pending_username'],
|
||||
'remember_me' => isset($_SESSION['2fa_pending_remember'])
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* class Settings
|
||||
*
|
||||
* Handles editing and fetching jilo configuration.
|
||||
*/
|
||||
class Settings {
|
||||
|
||||
/**
|
||||
* Loads javascript file the Jitsi server.
|
||||
*
|
||||
* @param string $jitsiUrl The base URL of the Jitsi server.
|
||||
* @param string $livejsFile The name of the remote js file to load.
|
||||
* @param bool $raw Whether to return the full file (true) or only uncommented values (false).
|
||||
*
|
||||
* @return string The content of the interface_config.js file or an error message.
|
||||
*/
|
||||
public function getPlatformJsFile($jitsiUrl, $livejsFile, $raw = false) {
|
||||
// constructing the URL
|
||||
$jsFile = $jitsiUrl . '/' . $livejsFile;
|
||||
|
||||
// default content, if we can't get the file contents
|
||||
$jsFileContent = "The file $livejsFile can't be loaded.";
|
||||
|
||||
// Check if URL is valid
|
||||
if (!filter_var($jsFile, FILTER_VALIDATE_URL)) {
|
||||
return "Invalid URL: $jsFile";
|
||||
}
|
||||
|
||||
// ssl options
|
||||
$contextOptions = [
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
];
|
||||
$context = stream_context_create($contextOptions);
|
||||
|
||||
// Try to get headers first to check if file exists and wasn't redirected
|
||||
$headers = @get_headers($jsFile, 1); // 1 to get headers as array
|
||||
if ($headers === false) {
|
||||
return "The file $livejsFile can't be loaded (connection error).";
|
||||
}
|
||||
|
||||
// Check for redirects
|
||||
$statusLine = $headers[0];
|
||||
if (strpos($statusLine, '301') !== false || strpos($statusLine, '302') !== false) {
|
||||
return "The file $livejsFile was redirected - this might indicate the file doesn't exist.";
|
||||
}
|
||||
|
||||
// Check if we got 200 OK
|
||||
if (strpos($statusLine, '200') === false) {
|
||||
return "The file $livejsFile can't be loaded (HTTP error: $statusLine).";
|
||||
}
|
||||
|
||||
// Check content type
|
||||
$contentType = isset($headers['Content-Type']) ? $headers['Content-Type'] : '';
|
||||
if (is_array($contentType)) {
|
||||
$contentType = end($contentType); // get last content-type in case of redirects
|
||||
}
|
||||
if (stripos($contentType, 'javascript') === false && stripos($contentType, 'text/plain') === false) {
|
||||
return "The file $livejsFile doesn't appear to be a JavaScript file (got $contentType).";
|
||||
}
|
||||
|
||||
// get the file
|
||||
$fileContent = @file_get_contents($jsFile, false, $context);
|
||||
|
||||
if ($fileContent !== false) {
|
||||
// Quick validation of content
|
||||
$firstLine = strtolower(trim(substr($fileContent, 0, 100)));
|
||||
if (strpos($firstLine, '<!doctype html>') !== false ||
|
||||
strpos($firstLine, '<html') !== false ||
|
||||
strpos($firstLine, '<?xml') !== false) {
|
||||
return "The file $livejsFile appears to be HTML/XML content instead of JavaScript.";
|
||||
}
|
||||
|
||||
// when we need only uncommented values
|
||||
if ($raw === false) {
|
||||
// remove block comments
|
||||
$jsFileContent = preg_replace('!/\*.*?\*/!s', '', $fileContent);
|
||||
// remove single-line comments
|
||||
$jsFileContent = preg_replace('/\/\/[^\n]*/', '', $jsFileContent);
|
||||
// remove empty lines
|
||||
$jsFileContent = preg_replace('/^\s*[\r\n]/m', '', $jsFileContent);
|
||||
|
||||
// when we need the full file as it is
|
||||
} else {
|
||||
$jsFileContent = $fileContent;
|
||||
}
|
||||
}
|
||||
|
||||
return $jsFileContent;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,420 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class TwoFactorAuthentication
|
||||
*
|
||||
* Handles two-factor authentication functionality using TOTP (Time-based One-Time Password).
|
||||
* Internal implementation without external dependencies.
|
||||
*/
|
||||
class TwoFactorAuthentication {
|
||||
private $db;
|
||||
private $secretLength = 20; // 160 bits for SHA1
|
||||
private $period = 30; // Time step in seconds (T0)
|
||||
private $digits = 6; // Number of digits in TOTP code
|
||||
private $algorithm = 'sha1'; // HMAC algorithm
|
||||
private $issuer = 'TotalMeet';
|
||||
private $window = 1; // Time window of 1 step before/after
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param PDO $database Database connection
|
||||
*/
|
||||
public function __construct($database) {
|
||||
if ($database instanceof PDO) {
|
||||
$this->db = $database;
|
||||
} else {
|
||||
$this->db = $database->getConnection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable 2FA for a user
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @param string $secret Secret key (base32 encoded)
|
||||
* @param string $code Verification code
|
||||
* @return bool True if enabled successfully
|
||||
*/
|
||||
public function enable($userId, $secret = null, $code = null) {
|
||||
try {
|
||||
// Check if 2FA is already enabled
|
||||
$stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existing && $existing['enabled']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no secret provided, generate one and return setup data
|
||||
if ($secret === null) {
|
||||
// Generate secret key
|
||||
$secret = $this->generateSecret();
|
||||
|
||||
// Get user's username for the QR code
|
||||
$stmt = $this->db->prepare('SELECT username FROM user WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Generate backup codes
|
||||
$backupCodes = $this->generateBackupCodes();
|
||||
|
||||
// Store in database without enabling yet
|
||||
$this->db->beginTransaction();
|
||||
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO user_2fa (user_id, secret_key, backup_codes, enabled, created_at)
|
||||
VALUES (?, ?, ?, 0, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
secret_key = VALUES(secret_key),
|
||||
backup_codes = VALUES(backup_codes),
|
||||
enabled = VALUES(enabled),
|
||||
created_at = VALUES(created_at)
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
$userId,
|
||||
$secret,
|
||||
json_encode($backupCodes)
|
||||
]);
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
// Generate otpauth URL for QR code
|
||||
$otpauthUrl = $this->generateOtpauthUrl($user['username'], $secret);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'secret' => $secret,
|
||||
'otpauthUrl' => $otpauthUrl,
|
||||
'backupCodes' => $backupCodes
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// If secret and code provided, verify the code and enable 2FA
|
||||
if ($code !== null) {
|
||||
// Verify the setup code
|
||||
if (!$this->verify($userId, $code)) {
|
||||
error_log("Code verification failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enable 2FA
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE user_2fa
|
||||
SET enabled = 1
|
||||
WHERE user_id = ? AND secret_key = ?
|
||||
');
|
||||
return $stmt->execute([$userId, $secret]);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($this->db->inTransaction()) {
|
||||
$this->db->rollBack();
|
||||
}
|
||||
error_log('2FA enable error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a 2FA code
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @param string $code The verification code
|
||||
* @return bool True if verified, false otherwise
|
||||
*/
|
||||
public function verify($userId, $code) {
|
||||
try {
|
||||
// Get user's 2FA settings
|
||||
$settings = $this->getUserSettings($userId);
|
||||
if (!$settings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if code matches a backup code
|
||||
if ($this->verifyBackupCode($userId, $code)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get current Unix timestamp
|
||||
$currentTime = time();
|
||||
|
||||
// Check time window
|
||||
for ($timeSlot = -$this->window; $timeSlot <= $this->window; $timeSlot++) {
|
||||
$checkTime = $currentTime + ($timeSlot * $this->period);
|
||||
$generatedCode = $this->generateCode($settings['secret_key'], $checkTime);
|
||||
if (hash_equals($generatedCode, $code)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('2FA verification error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random secret key
|
||||
*
|
||||
* @return string Base32 encoded secret
|
||||
*/
|
||||
private function generateSecret() {
|
||||
// Generate random bytes (160 bits for SHA1)
|
||||
$random = random_bytes($this->secretLength);
|
||||
return $this->base32Encode($random);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base32 encode data
|
||||
*
|
||||
* @param string $data Data to encode
|
||||
* @return string Base32 encoded string
|
||||
*/
|
||||
private function base32Encode($data) {
|
||||
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
$binary = '';
|
||||
$encoded = '';
|
||||
|
||||
// Convert to binary
|
||||
for ($i = 0; $i < strlen($data); $i++) {
|
||||
$binary .= str_pad(decbin(ord($data[$i])), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
// Process 5 bits at a time
|
||||
for ($i = 0; $i < strlen($binary); $i += 5) {
|
||||
$chunk = substr($binary, $i, 5);
|
||||
if (strlen($chunk) < 5) {
|
||||
$chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
|
||||
}
|
||||
$encoded .= $alphabet[bindec($chunk)];
|
||||
}
|
||||
|
||||
// Add padding
|
||||
$padding = strlen($encoded) % 8;
|
||||
if ($padding > 0) {
|
||||
$encoded .= str_repeat('=', 8 - $padding);
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base32 decode data
|
||||
*
|
||||
* @param string $data Base32 encoded string
|
||||
* @return string Decoded data
|
||||
*/
|
||||
private function base32Decode($data) {
|
||||
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
|
||||
// Remove padding and uppercase
|
||||
$data = rtrim(strtoupper($data), '=');
|
||||
|
||||
$binary = '';
|
||||
|
||||
// Convert to binary
|
||||
for ($i = 0; $i < strlen($data); $i++) {
|
||||
$position = strpos($alphabet, $data[$i]);
|
||||
if ($position === false) {
|
||||
continue;
|
||||
}
|
||||
$binary .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$decoded = '';
|
||||
// Process 8 bits at a time
|
||||
for ($i = 0; $i + 7 < strlen($binary); $i += 8) {
|
||||
$chunk = substr($binary, $i, 8);
|
||||
$decoded .= chr(bindec($chunk));
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a TOTP code for a given secret and time
|
||||
* RFC 6238 compliant implementation
|
||||
*/
|
||||
private function generateCode($secret, $time) {
|
||||
// Calculate number of time steps since Unix epoch
|
||||
$timeStep = (int)floor($time / $this->period);
|
||||
|
||||
// Pack time into 8 bytes (64-bit big-endian)
|
||||
$timeBin = pack('J', $timeStep);
|
||||
|
||||
// Clean secret of any padding
|
||||
$secret = rtrim($secret, '=');
|
||||
|
||||
// Get binary secret
|
||||
$secretBin = $this->base32Decode($secret);
|
||||
|
||||
// Calculate HMAC
|
||||
$hash = hash_hmac($this->algorithm, $timeBin, $secretBin, true);
|
||||
|
||||
// Get dynamic truncation offset
|
||||
$offset = ord($hash[strlen($hash) - 1]) & 0xF;
|
||||
|
||||
// Generate 31-bit number
|
||||
$code = (
|
||||
((ord($hash[$offset]) & 0x7F) << 24) |
|
||||
((ord($hash[$offset + 1]) & 0xFF) << 16) |
|
||||
((ord($hash[$offset + 2]) & 0xFF) << 8) |
|
||||
(ord($hash[$offset + 3]) & 0xFF)
|
||||
) % pow(10, $this->digits);
|
||||
|
||||
$code = str_pad($code, $this->digits, '0', STR_PAD_LEFT);
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate otpauth URL for QR codes
|
||||
* Format: otpauth://totp/ISSUER:ACCOUNT?secret=SECRET&issuer=ISSUER&algorithm=ALGORITHM&digits=DIGITS&period=PERIOD
|
||||
*/
|
||||
private function generateOtpauthUrl($username, $secret) {
|
||||
$params = [
|
||||
'secret' => $secret,
|
||||
'issuer' => $this->issuer,
|
||||
'algorithm' => strtoupper($this->algorithm),
|
||||
'digits' => $this->digits,
|
||||
'period' => $this->period
|
||||
];
|
||||
|
||||
return sprintf(
|
||||
'otpauth://totp/%s:%s?%s',
|
||||
rawurlencode($this->issuer),
|
||||
rawurlencode($username),
|
||||
http_build_query($params)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate backup codes
|
||||
*
|
||||
* @param int $count Number of backup codes to generate
|
||||
* @return array Array of backup codes
|
||||
*/
|
||||
private function generateBackupCodes($count = 8) {
|
||||
$codes = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$codes[] = bin2hex(random_bytes(4));
|
||||
}
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a backup code
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @param string $code The backup code to verify
|
||||
* @return bool True if verified, false otherwise
|
||||
*/
|
||||
private function verifyBackupCode($userId, $code) {
|
||||
try {
|
||||
$stmt = $this->db->prepare('SELECT backup_codes FROM user_2fa WHERE user_id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$backupCodes = json_decode($result['backup_codes'], true);
|
||||
|
||||
// Check if the code exists and hasn't been used
|
||||
$codeIndex = array_search($code, $backupCodes);
|
||||
if ($codeIndex !== false) {
|
||||
// Remove the used code
|
||||
unset($backupCodes[$codeIndex]);
|
||||
$backupCodes = array_values($backupCodes);
|
||||
|
||||
// Update backup codes in database
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE user_2fa
|
||||
SET backup_codes = ?
|
||||
WHERE user_id = ?
|
||||
');
|
||||
$stmt->execute([json_encode($backupCodes), $userId]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('Backup code verification error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable 2FA for a user
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @return bool True if disabled successfully
|
||||
*/
|
||||
public function disable($userId) {
|
||||
try {
|
||||
// First check if user has 2FA settings
|
||||
$settings = $this->getUserSettings($userId);
|
||||
if (!$settings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete the 2FA settings entirely instead of just disabling
|
||||
$stmt = $this->db->prepare('
|
||||
DELETE FROM user_2fa
|
||||
WHERE user_id = ?
|
||||
');
|
||||
return $stmt->execute([$userId]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('2FA disable error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if 2FA is enabled for a user
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @return bool True if enabled
|
||||
*/
|
||||
public function isEnabled($userId) {
|
||||
try {
|
||||
$stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $result && $result['enabled'];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('2FA status check error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getUserSettings($userId) {
|
||||
try {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT secret_key, backup_codes, enabled
|
||||
FROM user_2fa
|
||||
WHERE user_id = ?
|
||||
');
|
||||
$stmt->execute([$userId]);
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('Failed to get user 2FA settings: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* class User
|
||||
*
|
||||
* Handles user-related functionalities such as login, rights management, and profile updates.
|
||||
* Handles user-related functionalities such as registration, login, rights management, and profile updates.
|
||||
*/
|
||||
class User {
|
||||
/**
|
||||
|
@ -11,7 +11,6 @@ class User {
|
|||
*/
|
||||
private $db;
|
||||
private $rateLimiter;
|
||||
private $twoFactorAuth;
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
|
@ -20,16 +19,65 @@ class User {
|
|||
* @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 __DIR__ . '/ratelimiter.php';
|
||||
require_once __DIR__ . '/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 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -38,13 +86,15 @@ class User {
|
|||
*
|
||||
* @param string $username The username of the user.
|
||||
* @param string $password The password of the user.
|
||||
* @param string $twoFactorCode Optional. The 2FA code if 2FA is enabled.
|
||||
*
|
||||
* @return array Login result with status and any necessary data
|
||||
* @return bool True if login is successful, false otherwise.
|
||||
*/
|
||||
public function login($username, $password, $twoFactorCode = null) {
|
||||
// Get user's IP address
|
||||
$ipAddress = getUserIP();
|
||||
public function login($username, $password) {
|
||||
// get client IP address
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
|
||||
// Record attempt
|
||||
$this->rateLimiter->attempt($username, $ipAddress);
|
||||
|
||||
// Check rate limiting first
|
||||
if (!$this->rateLimiter->isAllowed($username, $ipAddress)) {
|
||||
|
@ -53,49 +103,20 @@ class User {
|
|||
}
|
||||
|
||||
// Then check credentials
|
||||
$query = $this->db->prepare("SELECT * FROM user WHERE username = :username");
|
||||
$query = $this->db->prepare("SELECT * FROM users WHERE username = :username");
|
||||
$query->bindParam(':username', $username);
|
||||
$query->execute();
|
||||
|
||||
$user = $query->fetch(PDO::FETCH_ASSOC);
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
// Check if 2FA is enabled
|
||||
if ($this->twoFactorAuth->isEnabled($user['id'])) {
|
||||
if ($twoFactorCode === null) {
|
||||
return [
|
||||
'status' => 'requires_2fa',
|
||||
'user_id' => $user['id'],
|
||||
'username' => $user['username']
|
||||
];
|
||||
}
|
||||
|
||||
// Verify 2FA code
|
||||
if (!$this->twoFactorAuth->verify($user['id'], $twoFactorCode)) {
|
||||
return [
|
||||
'status' => 'invalid_2fa',
|
||||
'message' => 'Invalid 2FA code'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Login successful
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['CREATED'] = time();
|
||||
$_SESSION['LAST_ACTIVITY'] = time();
|
||||
return [
|
||||
'status' => 'success',
|
||||
'user_id' => $user['id'],
|
||||
'username' => $user['username']
|
||||
];
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get remaining attempts AFTER this failed attempt
|
||||
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
|
||||
return [
|
||||
'status' => 'failed',
|
||||
'message' => "Invalid credentials. {$remainingAttempts} attempts remaining."
|
||||
];
|
||||
throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining.");
|
||||
}
|
||||
|
||||
|
||||
|
@ -108,7 +129,7 @@ class User {
|
|||
*/
|
||||
// FIXME not used now?
|
||||
public function getUserId($username) {
|
||||
$sql = 'SELECT id FROM user WHERE username = :username';
|
||||
$sql = 'SELECT id FROM users WHERE username = :username';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->bindParam(':username', $username);
|
||||
|
||||
|
@ -122,24 +143,24 @@ class User {
|
|||
/**
|
||||
* Fetches user details by user ID.
|
||||
*
|
||||
* @param int $userId The user ID.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return array|null User details or null if not found.
|
||||
*/
|
||||
public function getUserDetails($userId) {
|
||||
public function getUserDetails($user_id) {
|
||||
$sql = 'SELECT
|
||||
um.*,
|
||||
u.username
|
||||
FROM
|
||||
user_meta um
|
||||
LEFT JOIN user u
|
||||
users_meta um
|
||||
LEFT JOIN users u
|
||||
ON um.user_id = u.id
|
||||
WHERE
|
||||
u.id = :user_id';
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':user_id' => $userId,
|
||||
':user_id' => $user_id,
|
||||
]);
|
||||
|
||||
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
@ -150,19 +171,19 @@ class User {
|
|||
/**
|
||||
* Grants a user a specific right.
|
||||
*
|
||||
* @param int $userId The user ID.
|
||||
* @param int $user_id The user ID.
|
||||
* @param int $right_id The right ID to grant.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addUserRight($userId, $right_id) {
|
||||
$sql = 'INSERT INTO user_right
|
||||
public function addUserRight($user_id, $right_id) {
|
||||
$sql = 'INSERT INTO users_rights
|
||||
(user_id, right_id)
|
||||
VALUES
|
||||
(:user_id, :right_id)';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':user_id' => $userId,
|
||||
':user_id' => $user_id,
|
||||
':right_id' => $right_id,
|
||||
]);
|
||||
}
|
||||
|
@ -171,20 +192,20 @@ class User {
|
|||
/**
|
||||
* Revokes a specific right from a user.
|
||||
*
|
||||
* @param int $userId The user ID.
|
||||
* @param int $user_id The user ID.
|
||||
* @param int $right_id The right ID to revoke.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeUserRight($userId, $right_id) {
|
||||
$sql = 'DELETE FROM user_right
|
||||
public function removeUserRight($user_id, $right_id) {
|
||||
$sql = 'DELETE FROM users_rights
|
||||
WHERE
|
||||
user_id = :user_id
|
||||
AND
|
||||
right_id = :right_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':user_id' => $userId,
|
||||
':user_id' => $user_id,
|
||||
':right_id' => $right_id,
|
||||
]);
|
||||
}
|
||||
|
@ -199,7 +220,7 @@ class User {
|
|||
$sql = 'SELECT
|
||||
id AS right_id,
|
||||
name AS right_name
|
||||
FROM `right`
|
||||
FROM rights
|
||||
ORDER BY id ASC';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute();
|
||||
|
@ -212,27 +233,27 @@ class User {
|
|||
/**
|
||||
* Retrieves the rights assigned to a specific user.
|
||||
*
|
||||
* @param int $userId The user ID.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return array List of user rights.
|
||||
*/
|
||||
public function getUserRights($userId) {
|
||||
public function getUserRights($user_id) {
|
||||
$sql = 'SELECT
|
||||
u.id AS user_id,
|
||||
r.id AS right_id,
|
||||
r.name AS right_name
|
||||
FROM
|
||||
`user` u
|
||||
LEFT JOIN `user_right` ur
|
||||
users u
|
||||
LEFT JOIN users_rights ur
|
||||
ON u.id = ur.user_id
|
||||
LEFT JOIN `right` r
|
||||
LEFT JOIN rights r
|
||||
ON ur.right_id = r.id
|
||||
WHERE
|
||||
u.id = :user_id';
|
||||
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':user_id' => $userId,
|
||||
':user_id' => $user_id,
|
||||
]);
|
||||
|
||||
$result = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
@ -241,7 +262,7 @@ class User {
|
|||
$specialEntries = [];
|
||||
|
||||
// user 1 is always superuser
|
||||
if ($userId == 1) {
|
||||
if ($user_id == 1) {
|
||||
$specialEntries = [
|
||||
[
|
||||
'user_id' => 1,
|
||||
|
@ -251,7 +272,7 @@ class User {
|
|||
];
|
||||
|
||||
// user 2 is always demo
|
||||
} elseif ($userId == 2) {
|
||||
} elseif ($user_id == 2) {
|
||||
$specialEntries = [
|
||||
[
|
||||
'user_id' => 2,
|
||||
|
@ -275,17 +296,17 @@ class User {
|
|||
/**
|
||||
* Check if the user has a specific right.
|
||||
*
|
||||
* @param int $userId The user ID.
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $right_name The human-readable name of the user right.
|
||||
*
|
||||
* @return bool True if the user has the right, false otherwise.
|
||||
*/
|
||||
function hasRight($userId, $right_name) {
|
||||
$userRights = $this->getUserRights($userId);
|
||||
function hasRight($user_id, $right_name) {
|
||||
$userRights = $this->getUserRights($user_id);
|
||||
$userHasRight = false;
|
||||
|
||||
// superuser always has all the rights
|
||||
if ($userId === 1) {
|
||||
if ($user_id === 1) {
|
||||
$userHasRight = true;
|
||||
}
|
||||
|
||||
|
@ -304,7 +325,7 @@ class User {
|
|||
/**
|
||||
* Updates a user's metadata in the database.
|
||||
*
|
||||
* @param int $userId The ID of the user to update.
|
||||
* @param int $user_id The ID of the user to update.
|
||||
* @param array $updatedUser An associative array containing updated user data:
|
||||
* - 'name' (string): The updated name of the user.
|
||||
* - 'email' (string): The updated email of the user.
|
||||
|
@ -313,9 +334,9 @@ class User {
|
|||
*
|
||||
* @return bool|string Returns true if the update is successful, or an error message if an exception occurs.
|
||||
*/
|
||||
public function editUser($userId, $updatedUser) {
|
||||
public function editUser($user_id, $updatedUser) {
|
||||
try {
|
||||
$sql = 'UPDATE user_meta SET
|
||||
$sql = 'UPDATE users_meta SET
|
||||
name = :name,
|
||||
email = :email,
|
||||
timezone = :timezone,
|
||||
|
@ -323,7 +344,7 @@ class User {
|
|||
WHERE user_id = :user_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':user_id' => $userId,
|
||||
':user_id' => $user_id,
|
||||
':name' => $updatedUser['name'],
|
||||
':email' => $updatedUser['email'],
|
||||
':timezone' => $updatedUser['timezone'],
|
||||
|
@ -342,20 +363,20 @@ class User {
|
|||
/**
|
||||
* Removes a user's avatar from the database and deletes the associated file.
|
||||
*
|
||||
* @param int $userId The ID of the user whose avatar is being removed.
|
||||
* @param int $user_id The ID of the user whose avatar is being removed.
|
||||
* @param string $old_avatar Optional. The file path of the current avatar to delete. Default is an empty string.
|
||||
*
|
||||
* @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs.
|
||||
*/
|
||||
public function removeAvatar($userId, $old_avatar = '') {
|
||||
public function removeAvatar($user_id, $old_avatar = '') {
|
||||
try {
|
||||
// remove from database
|
||||
$sql = 'UPDATE user_meta SET
|
||||
$sql = 'UPDATE users_meta SET
|
||||
avatar = NULL
|
||||
WHERE user_id = :user_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':user_id' => $userId,
|
||||
':user_id' => $user_id,
|
||||
]);
|
||||
|
||||
// delete the old avatar file
|
||||
|
@ -375,14 +396,14 @@ class User {
|
|||
/**
|
||||
* Updates a user's avatar by uploading a new file and saving its path in the database.
|
||||
*
|
||||
* @param int $userId The ID of the user whose avatar is being updated.
|
||||
* @param int $user_id The ID of the user whose avatar is being updated.
|
||||
* @param array $avatar_file The uploaded avatar file from the $_FILES array.
|
||||
* Should include 'tmp_name', 'name', 'error', etc.
|
||||
* @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.
|
||||
*/
|
||||
public function changeAvatar($userId, $avatar_file, $avatars_path) {
|
||||
public function changeAvatar($user_id, $avatar_file, $avatars_path) {
|
||||
try {
|
||||
// check if the file was uploaded
|
||||
if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) {
|
||||
|
@ -399,13 +420,13 @@ class User {
|
|||
if (move_uploaded_file($fileTmpPath, $dest_path)) {
|
||||
try {
|
||||
// update user's avatar path in DB
|
||||
$sql = 'UPDATE user_meta SET
|
||||
$sql = 'UPDATE users_meta SET
|
||||
avatar = :avatar
|
||||
WHERE user_id = :user_id';
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([
|
||||
':avatar' => $newFileName,
|
||||
':user_id' => $userId
|
||||
':user_id' => $user_id
|
||||
]);
|
||||
// all went OK
|
||||
$_SESSION['notice'] .= 'Avatar updated successfully. ';
|
||||
|
@ -428,99 +449,6 @@ class User {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users for messaging
|
||||
*
|
||||
* @return array List of users with their IDs and usernames
|
||||
*/
|
||||
public function getUsers() {
|
||||
$sql = "SELECT id, username
|
||||
FROM `user`
|
||||
ORDER BY username ASC";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute();
|
||||
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable two-factor authentication for a user
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @param string $secret Secret key to use
|
||||
* @param string $code Verification code to validate
|
||||
* @return bool True if enabled successfully
|
||||
*/
|
||||
public function enableTwoFactor($userId, $secret = null, $code = null) {
|
||||
return $this->twoFactorAuth->enable($userId, $secret, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable two-factor authentication for a user
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @return bool True if disabled successfully
|
||||
*/
|
||||
public function disableTwoFactor($userId) {
|
||||
return $this->twoFactorAuth->disable($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a two-factor authentication code
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @param string $code The verification code
|
||||
* @return bool True if verified
|
||||
*/
|
||||
public function verifyTwoFactor($userId, $code) {
|
||||
return $this->twoFactorAuth->verify($userId, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two-factor authentication is enabled for a user
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @return bool True if enabled
|
||||
*/
|
||||
public function isTwoFactorEnabled($userId) {
|
||||
return $this->twoFactorAuth->isEnabled($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a user's password
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @param string $currentPassword Current password for verification
|
||||
* @param string $newPassword New password to set
|
||||
* @return bool True if password was changed successfully
|
||||
*/
|
||||
public function changePassword($userId, $currentPassword, $newPassword) {
|
||||
try {
|
||||
// First verify the current password
|
||||
$sql = "SELECT password FROM user WHERE id = :user_id";
|
||||
$query = $this->db->prepare($sql);
|
||||
$query->execute([':user_id' => $userId]);
|
||||
$user = $query->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user || !password_verify($currentPassword, $user['password'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hash the new password
|
||||
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
|
||||
// Update the password
|
||||
$sql = "UPDATE user SET password = :password WHERE id = :user_id";
|
||||
$query = $this->db->prepare($sql);
|
||||
return $query->execute([
|
||||
':password' => $hashedPassword,
|
||||
':user_id' => $userId
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error changing password: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
<?php
|
||||
|
||||
class Validator {
|
||||
private $errors = [];
|
||||
private $data = [];
|
||||
|
||||
public function __construct(array $data) {
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function validate(array $rules) {
|
||||
foreach ($rules as $field => $fieldRules) {
|
||||
foreach ($fieldRules as $rule => $parameter) {
|
||||
$this->applyRule($field, $rule, $parameter);
|
||||
}
|
||||
}
|
||||
return empty($this->errors);
|
||||
}
|
||||
|
||||
private function applyRule($field, $rule, $parameter) {
|
||||
$value = $this->data[$field] ?? null;
|
||||
|
||||
switch ($rule) {
|
||||
case 'required':
|
||||
if ($parameter && empty($value)) {
|
||||
$this->addError($field, "Field is required");
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->addError($field, "Invalid email format");
|
||||
}
|
||||
break;
|
||||
case 'min':
|
||||
if (!empty($value) && strlen($value) < $parameter) {
|
||||
$this->addError($field, "Minimum length is $parameter characters");
|
||||
}
|
||||
break;
|
||||
case 'max':
|
||||
if (!empty($value) && strlen($value) > $parameter) {
|
||||
$this->addError($field, "Maximum length is $parameter characters");
|
||||
}
|
||||
break;
|
||||
case 'numeric':
|
||||
if (!empty($value) && !is_numeric($value)) {
|
||||
$this->addError($field, "Must be a number");
|
||||
}
|
||||
break;
|
||||
case 'phone':
|
||||
if (!empty($value) && !preg_match('/^[+]?[\d\s-()]{7,}$/', $value)) {
|
||||
$this->addError($field, "Invalid phone number format");
|
||||
}
|
||||
break;
|
||||
case 'url':
|
||||
if (!empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) {
|
||||
$this->addError($field, "Invalid URL format");
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (!empty($value)) {
|
||||
$date = date_parse($value);
|
||||
if ($date['error_count'] > 0) {
|
||||
$this->addError($field, "Invalid date format");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'in':
|
||||
if (!empty($value) && !in_array($value, $parameter)) {
|
||||
$this->addError($field, "Invalid option selected");
|
||||
}
|
||||
break;
|
||||
case 'matches':
|
||||
if ($value !== ($this->data[$parameter] ?? null)) {
|
||||
$this->addError($field, "Does not match $parameter field");
|
||||
}
|
||||
break;
|
||||
case 'ip':
|
||||
if (!empty($value)) {
|
||||
// Support both IPv4 and IPv6
|
||||
if (!filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
|
||||
$this->addError($field, "Invalid IP address format");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function addError($field, $message) {
|
||||
if (!isset($this->errors[$field])) {
|
||||
$this->errors[$field] = [];
|
||||
}
|
||||
$this->errors[$field][] = $message;
|
||||
}
|
||||
|
||||
public function getErrors() {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function hasErrors() {
|
||||
return !empty($this->errors);
|
||||
}
|
||||
|
||||
public function getFirstError() {
|
||||
if (!$this->hasErrors()) {
|
||||
return null;
|
||||
}
|
||||
$firstField = array_key_first($this->errors);
|
||||
return $this->errors[$firstField][0];
|
||||
}
|
||||
}
|
|
@ -10,17 +10,6 @@ return [
|
|||
'domain' => 'localhost',
|
||||
// subfolder for the web app, if any
|
||||
'folder' => '/jilo-web/',
|
||||
// site name used in emails and in the interface
|
||||
'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
|
||||
'registration_enabled' => true,
|
||||
// will be displayed on login screen
|
||||
|
@ -31,27 +20,21 @@ return [
|
|||
//*******************************************
|
||||
|
||||
// database
|
||||
'db_type' => 'mariadb',
|
||||
|
||||
'sqlite' => [
|
||||
'db' => [
|
||||
// DB type for the web app, currently only "sqlite" is used
|
||||
'db_type' => 'sqlite',
|
||||
// default is ../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' => 'uploads/avatars/',
|
||||
// default avatar
|
||||
'default_avatar' => 'static/default_avatar.png',
|
||||
// system info
|
||||
'version' => '0.4',
|
||||
'version' => '0.3',
|
||||
// development has verbose error messages, production has not
|
||||
'environment' => 'development',
|
||||
|
||||
];
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
// Active theme (can be overridden by user preference)
|
||||
'active_theme' => 'default',
|
||||
|
||||
// Available themes with their display names
|
||||
'available_themes' => [
|
||||
'default' => 'Default built-in theme',
|
||||
'modern' => 'Modern theme',
|
||||
'retro' => 'Alternative retro theme'
|
||||
],
|
||||
|
||||
// Path configurations
|
||||
'paths' => [
|
||||
// Base directory for all external themes
|
||||
'themes' => __DIR__ . '/../../themes',
|
||||
|
||||
// Default templates location (built-in fallback)
|
||||
'templates' => __DIR__ . '/../templates',
|
||||
|
||||
// Public assets directory (built-in fallback)
|
||||
'public' => __DIR__ . '/../../public_html'
|
||||
],
|
||||
|
||||
// Theme configuration defaults
|
||||
'default_config' => [
|
||||
'name' => 'Unnamed Theme',
|
||||
'description' => 'A Jilo Web theme',
|
||||
'version' => '1.0.0',
|
||||
'author' => 'Lindeas Inc.',
|
||||
'screenshot' => 'screenshot.png',
|
||||
'options' => []
|
||||
]
|
||||
];
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class ConfigLoader
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private static $configPath = null;
|
||||
|
||||
/**
|
||||
* Load configuration array from a set of possible file locations.
|
||||
*
|
||||
* @param string[] $locations
|
||||
* @return array
|
||||
*/
|
||||
public static function loadConfig(array $locations): array
|
||||
{
|
||||
$configFile = null;
|
||||
foreach ($locations as $location) {
|
||||
if (file_exists($location)) {
|
||||
$configFile = $location;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$configFile) {
|
||||
die('Config file not found');
|
||||
}
|
||||
self::$configPath = $configFile;
|
||||
return require $configFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getConfigPath(): ?string
|
||||
{
|
||||
return self::$configPath;
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Exception;
|
||||
use Feedback;
|
||||
|
||||
class DatabaseConnector
|
||||
{
|
||||
/**
|
||||
* Connect to the database using given configuration and handle errors.
|
||||
*
|
||||
* @param array $config
|
||||
* @return mixed Database connection
|
||||
*/
|
||||
public static function connect(array $config)
|
||||
{
|
||||
// Load DB classes
|
||||
require_once __DIR__ . '/../classes/database.php';
|
||||
require_once __DIR__ . '/../includes/database.php';
|
||||
|
||||
try {
|
||||
$db = connectDB($config);
|
||||
if (!$db) {
|
||||
throw new Exception('Could not connect to database');
|
||||
}
|
||||
return $db;
|
||||
} catch (Exception $e) {
|
||||
// Show error and exit
|
||||
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to the database.', $e->getMessage()));
|
||||
include __DIR__ . '/../templates/page-header.php';
|
||||
include __DIR__ . '/../helpers/feedback.php';
|
||||
include __DIR__ . '/../templates/page-footer.php';
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class HookDispatcher
|
||||
{
|
||||
/**
|
||||
* Stores all registered hooks and their callbacks.
|
||||
* @var array<string, array<callable>>
|
||||
*/
|
||||
private static array $hooks = [];
|
||||
|
||||
/**
|
||||
* Register a callback for a given hook.
|
||||
*/
|
||||
public static function register(string $hook, callable $callback): void
|
||||
{
|
||||
if (!isset(self::$hooks[$hook])) {
|
||||
self::$hooks[$hook] = [];
|
||||
}
|
||||
self::$hooks[$hook][] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch all callbacks for the specified hook.
|
||||
*/
|
||||
public static function dispatch(string $hook, array $context = []): void
|
||||
{
|
||||
if (!empty(self::$hooks[$hook])) {
|
||||
foreach (self::$hooks[$hook] as $callback) {
|
||||
call_user_func($callback, $context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters for a hook key, passing a value through all callbacks.
|
||||
* Each callback should accept the value and return a modified value.
|
||||
*
|
||||
* @param string $hook
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public static function applyFilters(string $hook, $value)
|
||||
{
|
||||
if (!empty(self::$hooks[$hook])) {
|
||||
foreach (self::$hooks[$hook] as $callback) {
|
||||
$value = call_user_func($callback, $value);
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class MiddlewarePipeline {
|
||||
/** @var callable[] */
|
||||
private $middlewares = [];
|
||||
|
||||
/**
|
||||
* Add a middleware to the pipeline.
|
||||
* @param callable $middleware Should return false to halt execution.
|
||||
*/
|
||||
public function add(callable $middleware): void {
|
||||
$this->middlewares[] = $middleware;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all middlewares in sequence.
|
||||
* @return bool False if any middleware returns false, true otherwise.
|
||||
*/
|
||||
public function run(): bool {
|
||||
foreach ($this->middlewares as $middleware) {
|
||||
$result = call_user_func($middleware);
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
/**
|
||||
* NullLogger is a fallback for disabling logging when there is no logging plugin enabled.
|
||||
*/
|
||||
class NullLogger
|
||||
{
|
||||
/**
|
||||
* PSR-3 compatible log stub.
|
||||
* @param string $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function log(string $level, string $message, array $context = []): void {}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
/**
|
||||
* Loads all enabled plugins from the given directory.
|
||||
*
|
||||
* @param string $pluginsDir
|
||||
* @return array<string, array{path: string, meta: array}>
|
||||
*/
|
||||
public static function load(string $pluginsDir): array
|
||||
{
|
||||
$enabled = [];
|
||||
foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginPath) {
|
||||
$manifest = $pluginPath . '/plugin.json';
|
||||
if (!file_exists($manifest)) {
|
||||
continue;
|
||||
}
|
||||
$meta = json_decode(file_get_contents($manifest), true);
|
||||
if (empty($meta['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$name = basename($pluginPath);
|
||||
$enabled[$name] = [
|
||||
'path' => $pluginPath,
|
||||
'meta' => $meta,
|
||||
];
|
||||
$bootstrap = $pluginPath . '/bootstrap.php';
|
||||
if (file_exists($bootstrap)) {
|
||||
include_once $bootstrap;
|
||||
}
|
||||
}
|
||||
return $enabled;
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Session;
|
||||
use Feedback;
|
||||
|
||||
class Router {
|
||||
/**
|
||||
* Check session validity and handle redirection for protected pages.
|
||||
* Returns current username if session is valid, null otherwise.
|
||||
*/
|
||||
public static function checkAuth(array $config, string $app_root, array $public_pages, string $page): ?string {
|
||||
// Always allow login page to be accessed
|
||||
if ($page === 'login') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this is a public page
|
||||
$isPublicPage = in_array($page, $public_pages, true);
|
||||
|
||||
// For public pages, don't validate session
|
||||
if ($isPublicPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For protected pages, check if we have a valid session
|
||||
$validSession = Session::isValidSession(true);
|
||||
|
||||
// If session is valid, return the username
|
||||
if ($validSession) {
|
||||
return Session::getUsername();
|
||||
}
|
||||
|
||||
// If we get here, we need to redirect to login
|
||||
// Only show timeout message if we had an active session before
|
||||
if (isset($_SESSION['LAST_ACTIVITY']) && !isset($_SESSION['session_timeout_shown'])) {
|
||||
Feedback::flash('LOGIN', 'SESSION_TIMEOUT');
|
||||
$_SESSION['session_timeout_shown'] = true;
|
||||
}
|
||||
|
||||
// Preserve flash messages
|
||||
$flash_messages = $_SESSION['flash_messages'] ?? [];
|
||||
Session::cleanup($config);
|
||||
$_SESSION['flash_messages'] = $flash_messages;
|
||||
|
||||
// Build login URL with redirect if appropriate
|
||||
$loginUrl = $app_root . '?page=login';
|
||||
$trimmed = trim($page, '/?');
|
||||
if (!empty($trimmed) && !in_array($trimmed, INVALID_REDIRECT_PAGES, true)) {
|
||||
$loginUrl .= '&redirect=' . urlencode($_SERVER['REQUEST_URI']);
|
||||
}
|
||||
|
||||
header('Location: ' . $loginUrl);
|
||||
exit();
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Feedback Helper
|
||||
*
|
||||
* Combines functionality to handle retrieving and displaying feedback messages.
|
||||
*/
|
||||
|
||||
// Get any flash messages from previous request
|
||||
$flash_messages = Feedback::getFlash();
|
||||
if (!empty($flash_messages)) {
|
||||
$system_messages = array_merge($system_messages ?? [], array_map(function($flash) {
|
||||
return [
|
||||
'category' => $flash['category'],
|
||||
'key' => $flash['key'],
|
||||
'custom_message' => $flash['custom_message'] ?? null,
|
||||
'dismissible' => $flash['dismissible'] ?? false,
|
||||
'small' => $flash['small'] ?? false
|
||||
];
|
||||
}, $flash_messages));
|
||||
}
|
||||
|
||||
// Show feedback messages
|
||||
if (isset($system_messages) && is_array($system_messages)) {
|
||||
foreach ($system_messages as $msg) {
|
||||
echo Feedback::render(
|
||||
$msg['category'],
|
||||
$msg['key'],
|
||||
$msg['custom_message'] ?? null,
|
||||
$msg['dismissible'] ?? false,
|
||||
$msg['small'] ?? false
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Returns the user's IP address.
|
||||
* Uses global $user_IP set by Logger plugin if available, else falls back to server variables.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getUserIP() {
|
||||
global $user_IP;
|
||||
if (!empty($user_IP)) {
|
||||
return $user_IP;
|
||||
}
|
||||
// Fallback to HTTP headers or REMOTE_ADDR
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
return $_SERVER['HTTP_CLIENT_IP'];
|
||||
}
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
// May contain multiple IPs
|
||||
$parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($parts[0]);
|
||||
}
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Returns a logger instance: plugin Log if available, otherwise NullLogger.
|
||||
*
|
||||
* @param mixed $database Database or DatabaseConnector instance.
|
||||
* @return mixed Logger instance with PSR-3 log() compatible method.
|
||||
*/
|
||||
function getLoggerInstance($database) {
|
||||
if (class_exists('Log')) {
|
||||
return new Log($database);
|
||||
}
|
||||
require_once __DIR__ . '/../core/NullLogger.php';
|
||||
return new \App\Core\NullLogger();
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?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);
|
||||
}
|
||||
|
||||
?>
|
|
@ -44,3 +44,5 @@ function switchPlatform($platform_id) {
|
|||
// return the new URL with the new platform_id
|
||||
return $new_url;
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Security Helper
|
||||
*
|
||||
* Security helper, to be used with all the forms in the app.
|
||||
* Implements singleton pattern for consistent state management.
|
||||
*/
|
||||
class SecurityHelper {
|
||||
private static $instance = null;
|
||||
private $session;
|
||||
|
||||
private function __construct() {
|
||||
// Don't start a new session, just reference the existing one
|
||||
$this->session = &$_SESSION;
|
||||
}
|
||||
|
||||
public static function getInstance() {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new SecurityHelper();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
// Generate CSRF token
|
||||
public function generateCsrfToken() {
|
||||
if (empty($this->session['csrf_token'])) {
|
||||
$this->session['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $this->session['csrf_token'];
|
||||
}
|
||||
|
||||
// Verify CSRF token
|
||||
public function verifyCsrfToken($token) {
|
||||
if (empty($this->session['csrf_token']) || empty($token)) {
|
||||
return false;
|
||||
}
|
||||
return hash_equals($this->session['csrf_token'], $token);
|
||||
}
|
||||
|
||||
// Sanitize string input
|
||||
public function sanitizeString($input) {
|
||||
if (is_string($input)) {
|
||||
return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validate email
|
||||
public function validateEmail($email) {
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
// Validate integer
|
||||
public function validateInt($input) {
|
||||
return filter_var($input, FILTER_VALIDATE_INT) !== false;
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
public function validateUrl($url) {
|
||||
return filter_var($url, FILTER_VALIDATE_URL) !== false;
|
||||
}
|
||||
|
||||
// Sanitize array of inputs
|
||||
public function sanitizeArray($array, $allowedKeys = []) {
|
||||
$sanitized = [];
|
||||
foreach ($array as $key => $value) {
|
||||
if (empty($allowedKeys) || in_array($key, $allowedKeys)) {
|
||||
if (is_array($value)) {
|
||||
$sanitized[$key] = $this->sanitizeArray($value);
|
||||
} else {
|
||||
$sanitized[$key] = $this->sanitizeString($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
// Validate form data based on rules
|
||||
public function validateFormData($data, $rules) {
|
||||
$errors = [];
|
||||
foreach ($rules as $field => $rule) {
|
||||
if (!isset($data[$field]) && $rule['required']) {
|
||||
$errors[$field] = "Field is required";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($data[$field])) {
|
||||
$value = $data[$field];
|
||||
switch ($rule['type']) {
|
||||
case 'email':
|
||||
if (!$this->validateEmail($value)) {
|
||||
$errors[$field] = "Invalid email format";
|
||||
}
|
||||
break;
|
||||
case 'integer':
|
||||
if (!$this->validateInt($value)) {
|
||||
$errors[$field] = "Must be a valid integer";
|
||||
}
|
||||
break;
|
||||
case 'url':
|
||||
if (!$this->validateUrl($value)) {
|
||||
$errors[$field] = "Invalid URL format";
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
if (isset($rule['min']) && strlen($value) < $rule['min']) {
|
||||
$errors[$field] = "Minimum length is {$rule['min']} characters";
|
||||
}
|
||||
if (isset($rule['max']) && strlen($value) > $rule['max']) {
|
||||
$errors[$field] = "Maximum length is {$rule['max']} characters";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Theme Asset handler
|
||||
*
|
||||
* Serves theme assets (images, CSS, JS, etc.) securely by checking if the requested
|
||||
* theme and asset path are valid and accessible.
|
||||
*
|
||||
* This is a standalone handler that doesn't require the full application initialization.
|
||||
*/
|
||||
|
||||
// Set error reporting
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
// Define base path if not defined
|
||||
if (!defined('APP_ROOT')) {
|
||||
define('APP_ROOT', dirname(__DIR__));
|
||||
}
|
||||
|
||||
// Basic security checks
|
||||
if (!isset($_GET['theme']) || !preg_match('/^[a-zA-Z0-9_-]+$/', $_GET['theme'])) {
|
||||
http_response_code(400);
|
||||
exit('Invalid theme specified');
|
||||
}
|
||||
|
||||
if (!isset($_GET['path']) || empty($_GET['path'])) {
|
||||
http_response_code(400);
|
||||
exit('No asset path specified');
|
||||
}
|
||||
|
||||
$themeId = $_GET['theme'];
|
||||
$assetPath = $_GET['path'];
|
||||
|
||||
// Validate asset path (only alphanumeric, hyphen, underscore, dot, and forward slash)
|
||||
if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) {
|
||||
http_response_code(400);
|
||||
exit('Invalid asset path');
|
||||
}
|
||||
|
||||
// Prevent directory traversal
|
||||
if (strpos($assetPath, '..') !== false) {
|
||||
http_response_code(400);
|
||||
exit('Invalid asset path');
|
||||
}
|
||||
|
||||
// Build full path to the asset
|
||||
$themesDir = dirname(dirname(__DIR__)) . '/themes';
|
||||
$fullPath = realpath("$themesDir/$themeId/$assetPath");
|
||||
|
||||
// Additional security check to ensure the path is within the themes directory
|
||||
if ($fullPath === false) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain');
|
||||
error_log("Asset not found: $themesDir/$themeId/$assetPath");
|
||||
exit("Asset not found: $themesDir/$themeId/$assetPath");
|
||||
}
|
||||
|
||||
if (strpos($fullPath, realpath($themesDir)) !== 0) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: text/plain');
|
||||
error_log("Security violation: Attempted to access path outside themes directory: $fullPath");
|
||||
exit('Invalid asset path');
|
||||
}
|
||||
|
||||
// Check if the file exists and is readable
|
||||
if (!file_exists($fullPath) || !is_readable($fullPath)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain');
|
||||
error_log("File not found or not readable: $fullPath");
|
||||
exit("File not found or not readable: " . basename($fullPath));
|
||||
}
|
||||
|
||||
// Clear any previous output
|
||||
if (ob_get_level()) {
|
||||
ob_clean();
|
||||
}
|
||||
|
||||
// Determine content type based on file extension
|
||||
$extension = strtolower(pathinfo($assetPath, PATHINFO_EXTENSION));
|
||||
$contentTypes = [
|
||||
'css' => 'text/css',
|
||||
'js' => 'application/javascript',
|
||||
'json' => 'application/json',
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'gif' => 'image/gif',
|
||||
'svg' => 'image/svg+xml',
|
||||
'webp' => 'image/webp',
|
||||
'woff' => 'font/woff',
|
||||
'woff2' => 'font/woff2',
|
||||
'ttf' => 'font/ttf',
|
||||
'eot' => 'application/vnd.ms-fontobject',
|
||||
];
|
||||
|
||||
$contentType = $contentTypes[$extension] ?? 'application/octet-stream';
|
||||
|
||||
// Set proper headers
|
||||
header('Content-Type: ' . $contentType);
|
||||
header('Content-Length: ' . filesize($fullPath));
|
||||
|
||||
// Cache for 24 hours (86400 seconds)
|
||||
$expires = 86400;
|
||||
header('Cache-Control: public, max-age=' . $expires);
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
|
||||
header('Pragma: cache');
|
||||
|
||||
// Output the file
|
||||
readfile($fullPath);
|
|
@ -1,307 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Theme Helper
|
||||
*
|
||||
* Handles theme management and template/asset loading for the application.
|
||||
* Supports multiple themes with fallback to default theme when needed.
|
||||
* The default theme uses app/templates and public_html/static as fallbacks/
|
||||
*/
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Exception;
|
||||
|
||||
// Include Session class
|
||||
require_once __DIR__ . '/../classes/session.php';
|
||||
use Session;
|
||||
|
||||
class Theme
|
||||
{
|
||||
/**
|
||||
* @var array Theme configuration
|
||||
*/
|
||||
private static $config;
|
||||
|
||||
|
||||
/**
|
||||
* Get the theme configuration
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getConfig()
|
||||
{
|
||||
// Always reload the config to get the latest changes
|
||||
self::$config = require __DIR__ . '/../config/theme.php';
|
||||
return self::$config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @var string Current theme name
|
||||
*/
|
||||
private static $currentTheme;
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the theme system
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
// Only load config if not already loaded
|
||||
if (self::$config === null) {
|
||||
self::$config = require __DIR__ . '/../config/theme.php';
|
||||
}
|
||||
self::$currentTheme = self::getCurrentThemeName();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current theme name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getCurrentThemeName()
|
||||
{
|
||||
// Ensure session is started
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
Session::startSession();
|
||||
}
|
||||
|
||||
// Check if already determined
|
||||
if (self::$currentTheme !== null) {
|
||||
return self::$currentTheme;
|
||||
}
|
||||
|
||||
// Try to get from session first
|
||||
$sessionTheme = isset($_SESSION['theme']) ? $_SESSION['theme'] : null;
|
||||
if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) {
|
||||
self::$currentTheme = $sessionTheme;
|
||||
} else {
|
||||
// Fall back to default theme
|
||||
self::$currentTheme = self::$config['active_theme'];
|
||||
}
|
||||
|
||||
return self::$currentTheme;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL for a theme asset
|
||||
*
|
||||
* @param string $themeId Theme ID
|
||||
* @param string $assetPath Path to the asset relative to theme directory (e.g., 'css/style.css')
|
||||
* @return string|null URL to the asset or null if not found
|
||||
*/
|
||||
public static function getAssetUrl($themeId, $assetPath = '')
|
||||
{
|
||||
// Clean and validate the asset path
|
||||
$assetPath = ltrim($assetPath, '/');
|
||||
if (empty($assetPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only allow alphanumeric, hyphen, underscore, dot, and forward slash
|
||||
if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prevent directory traversal
|
||||
if (strpos($assetPath, '..') !== false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fullPath = __DIR__ . "/../../themes/$themeId/$assetPath";
|
||||
if (!file_exists($fullPath) || !is_readable($fullPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate URL that goes through index.php
|
||||
global $app_root;
|
||||
// Remove any trailing slash from app_root to avoid double slashes
|
||||
$baseUrl = rtrim($app_root, '/');
|
||||
return "$baseUrl/?page=theme-asset&theme=" . urlencode($themeId) . "&path=" . urlencode($assetPath);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the current theme for the session
|
||||
*
|
||||
* @param string $themeName
|
||||
* @return bool
|
||||
*/
|
||||
public static function setCurrentTheme(string $themeName): bool
|
||||
{
|
||||
if (!self::themeExists($themeName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update session
|
||||
if (Session::isValidSession()) {
|
||||
$_SESSION['theme'] = $themeName;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear the current theme cache
|
||||
self::$currentTheme = null;
|
||||
|
||||
// Update config file
|
||||
$configFile = __DIR__ . '/../config/theme.php';
|
||||
if (file_exists($configFile) && is_writable($configFile)) {
|
||||
$config = file_get_contents($configFile);
|
||||
// Update the active_theme in the config
|
||||
$newConfig = preg_replace(
|
||||
"/'active_theme'\s*=>\s*'[^']*'/",
|
||||
"'active_theme' => '" . addslashes($themeName) . "'",
|
||||
$config
|
||||
);
|
||||
|
||||
if ($newConfig !== $config) {
|
||||
if (file_put_contents($configFile, $newConfig) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self::$currentTheme = $themeName;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a theme exists
|
||||
*
|
||||
* @param string $themeName
|
||||
* @return bool
|
||||
*/
|
||||
public static function themeExists(string $themeName): bool
|
||||
{
|
||||
// Default theme always exists as it uses core templates
|
||||
if ($themeName === 'default') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$themePath = self::getThemePath($themeName);
|
||||
return is_dir($themePath) && file_exists("$themePath/config.php");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the path to a theme
|
||||
*
|
||||
* @param string|null $themeName
|
||||
* @return string
|
||||
*/
|
||||
public static function getThemePath(?string $themeName = null): string
|
||||
{
|
||||
$themeName = $themeName ?? self::getCurrentThemeName();
|
||||
$config = self::getConfig();
|
||||
return rtrim($config['paths']['themes'], '/') . "/$themeName";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL for a theme asset
|
||||
*
|
||||
* @param string $path
|
||||
* @param bool $includeVersion
|
||||
* @return string
|
||||
*/
|
||||
public static function asset($path, $includeVersion = false)
|
||||
{
|
||||
$themeName = self::getCurrentThemeName();
|
||||
$config = self::getConfig();
|
||||
$baseUrl = rtrim($GLOBALS['app_root'] ?? '', '/');
|
||||
|
||||
// For non-default themes, use theme assets
|
||||
if ($themeName !== 'default') {
|
||||
$assetPath = "/themes/{$themeName}/assets/" . ltrim($path, '/');
|
||||
|
||||
// Add version query string for cache busting
|
||||
if ($includeVersion) {
|
||||
$version = self::getThemeVersion($themeName);
|
||||
$assetPath .= (strpos($assetPath, '?') !== false ? '&' : '?') . 'v=' . $version;
|
||||
}
|
||||
} else {
|
||||
// For default theme, use public_html directly
|
||||
$assetPath = '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
return $baseUrl . $assetPath;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Include a theme template file
|
||||
*
|
||||
* @param string $template Template name without .php extension
|
||||
* @return void
|
||||
*/
|
||||
public static function include($template)
|
||||
{
|
||||
global $config;
|
||||
$config = $config ?? [];
|
||||
|
||||
$themeConfig = self::getConfig();
|
||||
$themeName = self::getCurrentThemeName();
|
||||
|
||||
// We need this here, otherwise because this helper
|
||||
// between index and the views breaks the session vars
|
||||
extract($GLOBALS, EXTR_SKIP | EXTR_REFS);
|
||||
|
||||
// Ensure config is always available in templates
|
||||
$config = array_merge($config, $themeConfig);
|
||||
|
||||
// For non-default themes, look in the theme directory first
|
||||
if ($themeName !== 'default') {
|
||||
$themePath = $config['paths']['themes'] . '/' . $themeName . '/views/' . $template . '.php';
|
||||
if (file_exists($themePath)) {
|
||||
include $themePath;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default template location
|
||||
$defaultPath = $config['paths']['templates'] . '/' . $template . '.php';
|
||||
if (file_exists($defaultPath)) {
|
||||
include $defaultPath;
|
||||
return;
|
||||
}
|
||||
|
||||
// Log error if template not found
|
||||
error_log("Template not found: {$template} in theme: {$themeName}");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all available themes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAvailableThemes(): array
|
||||
{
|
||||
$config = self::getConfig();
|
||||
$availableThemes = $config['available_themes'] ?? [];
|
||||
$themes = [];
|
||||
|
||||
// Add default theme if not already present
|
||||
if (!isset($availableThemes['default'])) {
|
||||
$availableThemes['default'] = 'Default built-in theme';
|
||||
}
|
||||
|
||||
// Verify each theme exists and has a config file
|
||||
$themesDir = $config['paths']['themes'] ?? (__DIR__ . '/../../themes');
|
||||
foreach ($availableThemes as $id => $name) {
|
||||
if ($id === 'default' || (is_dir("$themesDir/$id") && file_exists("$themesDir/$id/config.php"))) {
|
||||
$themes[$id] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
return $themes;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the theme system
|
||||
Theme::init();
|
|
@ -11,3 +11,5 @@ if (!isset($until_time) || (isset($until_time) && $until_time == '')) {
|
|||
} else {
|
||||
$time_range_specified = true;
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<?php
|
||||
// Pages that should not be used as redirect targets
|
||||
const INVALID_REDIRECT_PAGES = [
|
||||
'', 'login', 'logout', 'register', 'dashboard', '/'
|
||||
];
|
|
@ -1,52 +0,0 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/../helpers/security.php';
|
||||
|
||||
function applyCsrfMiddleware() {
|
||||
global $logObject, $user_IP;
|
||||
$security = SecurityHelper::getInstance();
|
||||
|
||||
// Skip CSRF check for GET requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip CSRF check for initial login, registration, and 2FA verification attempts
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' &&
|
||||
isset($_GET['page']) && isset($_GET['action']) &&
|
||||
$_GET['page'] === 'login' && $_GET['action'] === 'verify' &&
|
||||
isset($_SESSION['2fa_pending_user_id'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip CSRF check for initial login and registration attempts
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' &&
|
||||
isset($_GET['page']) &&
|
||||
in_array($_GET['page'], ['login', 'register']) &&
|
||||
!isset($_SESSION['username'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check CSRF token for all other POST requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Check for token in POST data or headers
|
||||
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!$security->verifyCsrfToken($token)) {
|
||||
// Log CSRF attempt
|
||||
$ipAddress = $user_IP;
|
||||
$logMessage = sprintf(
|
||||
"CSRF attempt detected - IP: %s, Page: %s, User: %s",
|
||||
$ipAddress,
|
||||
$_GET['page'] ?? 'unknown',
|
||||
$_SESSION['username'] ?? 'anonymous'
|
||||
);
|
||||
$logObject->log('error', $logMessage, ['user_id' => null, 'scope' => 'system']);
|
||||
|
||||
// Return error message
|
||||
http_response_code(403);
|
||||
die('Invalid CSRF token. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
<?php
|
||||
$token = SecurityHelper::getInstance()->generateCsrfToken();
|
||||
?>
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token) ?>" />
|
|
@ -1,53 +1,10 @@
|
|||
<?php
|
||||
|
||||
// connect to database
|
||||
function connectDB($config) {
|
||||
// sqlite database file
|
||||
if ($config['db_type'] === 'sqlite') {
|
||||
try {
|
||||
$dbFile = $config['sqlite']['sqlite_file'] ?? null;
|
||||
if (!$dbFile || !file_exists($dbFile)) {
|
||||
throw new Exception(getError("Database file \"{$dbFile}\"not found."));
|
||||
}
|
||||
$db = new Database([
|
||||
'type' => $config['db_type'],
|
||||
'dbFile' => $dbFile,
|
||||
]);
|
||||
$pdo = $db->getConnection();
|
||||
} catch (Exception $e) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to DB.', $e->getMessage()));
|
||||
return false;
|
||||
}
|
||||
return $db;
|
||||
function connectDB($config, $database = '', $dbFile = '', $platformId = '') {
|
||||
|
||||
// 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 {
|
||||
Feedback::flash('ERROR', 'DEFAULT', getError("Error: unknown database type \"{$config['db_type']}\""));
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// connect to Jilo database
|
||||
function connectJiloDB($config, $dbFile = '', $platformId = '') {
|
||||
// connecting ti a jilo sqlite database
|
||||
if ($database === 'jilo') {
|
||||
try {
|
||||
if (!$dbFile || !file_exists($dbFile)) {
|
||||
throw new Exception(getError("Invalid platform ID \"{$platformId}\", database file \"{$dbFile}\" not found."));
|
||||
|
@ -60,4 +17,46 @@ function connectJiloDB($config, $dbFile = '', $platformId = '') {
|
|||
} catch (Exception $e) {
|
||||
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
|
||||
}
|
||||
|
||||
// connecting to a jilo-web database of the web app
|
||||
} else {
|
||||
|
||||
// sqlite database file
|
||||
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']}\"";
|
||||
Messages::flash('ERROR', 'DEFAULT', $error);
|
||||
exit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -38,3 +38,5 @@ function renderMessage(&$message, $type, $unset = false) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
if (isset($messages) && is_array($messages)) {
|
||||
foreach ($messages as $msg) {
|
||||
echo Messages::render($msg['category'], $msg['key'], $msg['custom_message'] ?? null, $msg['dismissible'] ?? false, $msg['small'] ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
|
@ -2,44 +2,32 @@
|
|||
|
||||
// Message strings for translation
|
||||
return [
|
||||
'ERROR' => [
|
||||
'CSRF_INVALID' => 'Invalid security token. Please try again.',
|
||||
'INVALID_ACTION' => 'Invalid action requested.',
|
||||
'DEFAULT' => 'An error occurred. Please try again.',
|
||||
],
|
||||
'LOGIN' => [
|
||||
'LOGIN_SUCCESS' => 'Login successful.',
|
||||
'LOGIN_FAILED' => 'Login failed. Please check your credentials.',
|
||||
'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_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.',
|
||||
'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.',
|
||||
],
|
||||
'SECURITY' => [
|
||||
'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.',
|
||||
'WHITELIST_ADD_ERROR' => 'Failed to add IP to whitelist. Please check the IP format.',
|
||||
'WHITELIST_REMOVE_SUCCESS' => 'IP address successfully removed from whitelist.',
|
||||
'WHITELIST_REMOVE_ERROR' => 'Failed to remove IP from whitelist.',
|
||||
'BLACKLIST_ADD_SUCCESS' => 'IP address successfully added to blacklist.',
|
||||
'BLACKLIST_ADD_ERROR' => 'Failed to add IP to blacklist. Please check the IP format.',
|
||||
'BLACKLIST_REMOVE_SUCCESS' => 'IP address successfully removed from blacklist.',
|
||||
'BLACKLIST_REMOVE_ERROR' => 'Failed to remove IP from blacklist.',
|
||||
'RATE_LIMIT_INFO' => 'Rate limiting is active. This helps protect against brute force attacks.',
|
||||
'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.',
|
||||
'IP_REQUIRED' => 'IP address is required.',
|
||||
],
|
||||
'REGISTER' => [
|
||||
'SUCCESS' => 'Registration successful. You can log in now.',
|
||||
'FAILED' => 'Registration failed: %s',
|
||||
'DISABLED' => 'Registration is disabled.',
|
||||
],
|
||||
'SECURITY' => [
|
||||
'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.',
|
||||
'WHITELIST_ADD_FAILED' => 'Failed to add IP to whitelist.',
|
||||
'WHITELIST_ADD_ERROR_IP' => 'Failed to add IP to whitelist. Please check the IP format.',
|
||||
'WHITELIST_REMOVE_SUCCESS' => 'IP address successfully removed from whitelist.',
|
||||
'WHITELIST_REMOVE_FAILED' => 'Failed to remove IP from whitelist.',
|
||||
'BLACKLIST_ADD_SUCCESS' => 'IP address successfully added to blacklist.',
|
||||
'BLACKLIST_ADD_FAILED' => 'Failed to add IP to blacklist.',
|
||||
'BLACKLIST_ADD_ERROR_IP' => 'Failed to add IP to blacklist. Please check the IP format.',
|
||||
'BLACKLIST_REMOVE_SUCCESS' => 'IP address successfully removed from blacklist.',
|
||||
'BLACKLIST_REMOVE_FAILED' => 'Failed to remove IP from blacklist.',
|
||||
'RATE_LIMIT_INFO' => 'Rate limiting is active. This helps protect against brute force attacks.',
|
||||
'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.',
|
||||
'IP_REQUIRED' => 'IP address is required.',
|
||||
],
|
||||
'THEME' => [
|
||||
'THEME_CHANGE_SUCCESS' => 'Theme has been changed successfully.',
|
||||
'THEME_CHANGE_FAILED' => 'Failed to change theme. The selected theme may not be available.',
|
||||
],
|
||||
'SYSTEM' => [
|
||||
'DB_ERROR' => 'Error connecting to the database: %s',
|
||||
'DB_CONNECT_ERROR' => 'Error connecting to DB: %s',
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
// Get any flash messages from previous request
|
||||
$flash_messages = Messages::getFlash();
|
||||
if (!empty($flash_messages)) {
|
||||
$messages = array_merge($messages, array_map(function($flash) {
|
||||
return [
|
||||
'category' => $flash['category'],
|
||||
'key' => $flash['key'],
|
||||
'custom_message' => $flash['custom_message'] ?? null,
|
||||
'dismissible' => $flash['dismissible'] ?? false,
|
||||
'small' => $flash['small'] ?? false
|
||||
];
|
||||
}, $flash_messages));
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/../classes/ratelimiter.php';
|
||||
|
||||
/**
|
||||
* Rate limit middleware for page requests
|
||||
*
|
||||
* @param Database $database Database connection
|
||||
* @param string $endpoint The endpoint being accessed
|
||||
* @param int|null $userId Current user ID if authenticated
|
||||
* @param RateLimiter|null $existingRateLimiter Optional existing RateLimiter instance
|
||||
* @return bool True if request is allowed, false if rate limited
|
||||
*/
|
||||
function checkRateLimit($database, $endpoint, $userId = null, $existingRateLimiter = null) {
|
||||
global $app_root, $user_IP;
|
||||
$isTest = defined('PHPUNIT_RUNNING');
|
||||
$rateLimiter = $existingRateLimiter ?? new RateLimiter($database);
|
||||
$ipAddress = $user_IP;
|
||||
|
||||
// Check if request is allowed
|
||||
if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) {
|
||||
// Get remaining requests for error message
|
||||
$remaining = $rateLimiter->getRemainingPageRequests($ipAddress, $endpoint, $userId);
|
||||
|
||||
if (!$isTest) {
|
||||
// Set rate limit headers
|
||||
header('X-RateLimit-Remaining: ' . $remaining);
|
||||
header('X-RateLimit-Reset: ' . (time() + 60)); // Reset in 1 minute
|
||||
|
||||
// Return 429 Too Many Requests
|
||||
http_response_code(429);
|
||||
|
||||
// If AJAX request, return JSON
|
||||
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Too many requests. Please try again in a minute.',
|
||||
'messageData' => Feedback::getMessageData('ERROR', 'DEFAULT', 'Too many requests. Please try again in a minute.', true)
|
||||
]);
|
||||
} else {
|
||||
// For regular requests, set flash message and redirect
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Too many requests. Please try again in a minute.', true);
|
||||
header('Location: ' . htmlspecialchars($app_root));
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// In test mode, just set the flash message
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Too many requests. Please try again in a minute.', true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Record this request
|
||||
$rateLimiter->recordPageRequest($ipAddress, $endpoint);
|
||||
return true;
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
// sanitize all input vars that may end up in URLs or forms
|
||||
|
||||
$platform_id = htmlspecialchars($_REQUEST['platform'] ?? '');
|
||||
$platform_id = htmlspecialchars($_REQUEST['platform']);
|
||||
if (isset($_REQUEST['page'])) {
|
||||
$page = htmlspecialchars($_REQUEST['page']);
|
||||
} else {
|
||||
|
@ -59,3 +59,6 @@ if (isset($_POST['check_period'])) {
|
|||
if (isset($_POST['name'])) {
|
||||
$name = htmlspecialchars($_POST['name']);
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Security Headers Middleware
|
||||
*
|
||||
* Sets various security headers to protect against common web vulnerabilities:
|
||||
* - HSTS: Force HTTPS connections
|
||||
* - CSP: Content Security Policy to prevent XSS and other injection attacks
|
||||
* - X-Frame-Options: Prevent clickjacking
|
||||
* - X-Content-Type-Options: Prevent MIME-type sniffing
|
||||
* - Referrer-Policy: Control referrer information
|
||||
* - Permissions-Policy: Control browser features
|
||||
*/
|
||||
|
||||
function applySecurityHeaders($testMode = false) {
|
||||
$headers = [];
|
||||
|
||||
// Get current page
|
||||
$current_page = $_GET['page'] ?? 'dashboard';
|
||||
|
||||
// Define pages that need media access
|
||||
$media_enabled_pages = [
|
||||
// 'conference' => ['camera', 'microphone'],
|
||||
// 'call' => ['microphone'],
|
||||
// Add more pages and their required permissions as needed
|
||||
];
|
||||
|
||||
// Strict Transport Security (HSTS)
|
||||
// Only enable if HTTPS is properly configured
|
||||
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
|
||||
$headers[] = 'Strict-Transport-Security: max-age=31536000; includeSubDomains; preload';
|
||||
}
|
||||
|
||||
// Content Security Policy (CSP)
|
||||
$csp = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Required for Bootstrap and jQuery
|
||||
"style-src 'self' 'unsafe-inline' https://use.fontawesome.com", // Allow FontAwesome CSS
|
||||
"img-src 'self' data:", // Allow data: URLs for images
|
||||
"font-src 'self' https://use.fontawesome.com", // Allow FontAwesome fonts
|
||||
"connect-src 'self'",
|
||||
"frame-ancestors 'none'", // Equivalent to X-Frame-Options: DENY
|
||||
"form-action 'self'",
|
||||
"base-uri 'self'",
|
||||
"upgrade-insecure-requests" // Force HTTPS for all requests
|
||||
];
|
||||
$headers[] = "Content-Security-Policy: " . implode('; ', $csp);
|
||||
|
||||
// X-Frame-Options (legacy support)
|
||||
$headers[] = 'X-Frame-Options: DENY';
|
||||
|
||||
// X-Content-Type-Options
|
||||
$headers[] = 'X-Content-Type-Options: nosniff';
|
||||
|
||||
// X-XSS-Protection
|
||||
$headers[] = 'X-XSS-Protection: 1; mode=block';
|
||||
|
||||
// Referrer-Policy
|
||||
$headers[] = 'Referrer-Policy: strict-origin-when-cross-origin';
|
||||
|
||||
// Permissions-Policy
|
||||
$permissions = [
|
||||
'geolocation=()',
|
||||
'payment=()',
|
||||
'usb=()',
|
||||
'accelerometer=()',
|
||||
'autoplay=()',
|
||||
'document-domain=()',
|
||||
'encrypted-media=()',
|
||||
'fullscreen=(self)',
|
||||
'magnetometer=()',
|
||||
'midi=()',
|
||||
'sync-xhr=(self)',
|
||||
'usb=()'
|
||||
];
|
||||
|
||||
// Add camera/microphone permissions based on current page
|
||||
$camera_allowed = false;
|
||||
$microphone_allowed = false;
|
||||
|
||||
if (isset($media_enabled_pages[$current_page])) {
|
||||
$allowed_media = $media_enabled_pages[$current_page];
|
||||
if (in_array('camera', $allowed_media)) {
|
||||
$camera_allowed = true;
|
||||
}
|
||||
if (in_array('microphone', $allowed_media)) {
|
||||
$microphone_allowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add media permissions
|
||||
$permissions[] = $camera_allowed ? 'camera=(self)' : 'camera=()';
|
||||
$permissions[] = $microphone_allowed ? 'microphone=(self)' : 'microphone=()';
|
||||
|
||||
$headers[] = 'Permissions-Policy: ' . implode(', ', $permissions);
|
||||
|
||||
// Clear PHP version
|
||||
if (!$testMode) {
|
||||
header_remove('X-Powered-By');
|
||||
}
|
||||
|
||||
// Prevent caching of sensitive pages
|
||||
if (in_array($current_page, ['login', 'register', 'profile', 'security'])) {
|
||||
$headers[] = 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0';
|
||||
$headers[] = 'Pragma: no-cache';
|
||||
$headers[] = 'Expires: ' . gmdate('D, d M Y H:i:s', time() - 3600) . ' GMT';
|
||||
}
|
||||
|
||||
if ($testMode) {
|
||||
return $headers;
|
||||
}
|
||||
|
||||
// Apply headers in production
|
||||
foreach ($headers as $header) {
|
||||
header($header);
|
||||
}
|
||||
}
|
|
@ -8,167 +8,61 @@
|
|||
* to allow time-based invalidation if needed.
|
||||
*/
|
||||
|
||||
// Constants for session keys and cache settings
|
||||
define('SESSION_CACHE_SUFFIX', '_cache');
|
||||
define('SESSION_CACHE_TIME_SUFFIX', '_cache_time');
|
||||
define('CACHE_EXPIRY_TIME', 3600); // 1 hour in seconds
|
||||
|
||||
// Input validation
|
||||
$action = isset($_GET['action']) ? htmlspecialchars(trim($_GET['action']), ENT_QUOTES, 'UTF-8') : '';
|
||||
$agentId = filter_input(INPUT_GET, 'agent', FILTER_VALIDATE_INT);
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$agent = $_REQUEST['agent'] ?? '';
|
||||
require '../app/classes/agent.php';
|
||||
require '../app/classes/host.php';
|
||||
$agentObject = new Agent($db);
|
||||
$hostObject = new Host($db);
|
||||
|
||||
/**
|
||||
* Get the cache key for an agent
|
||||
* @param int $agentId The agent ID
|
||||
* @param string $suffix The suffix to append (_cache or _cache_time)
|
||||
* @return string The cache key
|
||||
*/
|
||||
function getAgentCacheKey($agentId, $suffix) {
|
||||
return "agent{$agentId}{$suffix}";
|
||||
}
|
||||
$agentObject = new Agent($dbWeb);
|
||||
|
||||
/**
|
||||
* Check if cache is expired
|
||||
* @param int $agentId The agent ID
|
||||
* @return bool True if cache is expired or doesn't exist
|
||||
*/
|
||||
function isCacheExpired($agentId) {
|
||||
$timeKey = getAgentCacheKey($agentId, SESSION_CACHE_TIME_SUFFIX);
|
||||
if (!isset($_SESSION[$timeKey])) {
|
||||
return true;
|
||||
}
|
||||
return (time() - $_SESSION[$timeKey]) > CACHE_EXPIRY_TIME;
|
||||
}
|
||||
// if it's a POST request, it's saving to cache
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
|
||||
// Handle POST request (saving to cache)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// read the JSON sent from javascript
|
||||
$data = file_get_contents("php://input");
|
||||
$result = json_decode($data, true);
|
||||
|
||||
// Apply rate limiting for adding new contacts
|
||||
require '../app/includes/rate_limit_middleware.php';
|
||||
checkRateLimit($db, 'contact', $userId);
|
||||
|
||||
// Validate agent ID for POST operations
|
||||
if ($agentId === false || $agentId === null) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid agent ID format');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid agent ID format']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read and validate JSON data
|
||||
$jsonData = file_get_contents("php://input");
|
||||
if ($jsonData === false) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Failed to read input data');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to read input data']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode($jsonData, true);
|
||||
|
||||
// Handle cache clearing
|
||||
if ($data === null && !empty($agentId)) {
|
||||
$cacheKey = getAgentCacheKey($agentId, SESSION_CACHE_SUFFIX);
|
||||
$timeKey = getAgentCacheKey($agentId, SESSION_CACHE_TIME_SUFFIX);
|
||||
|
||||
unset($_SESSION[$cacheKey]);
|
||||
unset($_SESSION[$timeKey]);
|
||||
|
||||
Feedback::flash('SUCCESS', 'DEFAULT', "Cache for agent {$agentId} is cleared.");
|
||||
// store the data in the session
|
||||
if ($result) {
|
||||
$_SESSION["agent{$agent}_cache"] = $result;
|
||||
$_SESSION["agent{$agent}_cache_time"] = time(); // store the cache time
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => "Cache for agent {$agentId} is cleared."
|
||||
'message' => "Cache for agent {$agent} is stored."
|
||||
]);
|
||||
}
|
||||
// Handle cache storing
|
||||
elseif ($data) {
|
||||
$cacheKey = getAgentCacheKey($agentId, SESSION_CACHE_SUFFIX);
|
||||
$timeKey = getAgentCacheKey($agentId, SESSION_CACHE_TIME_SUFFIX);
|
||||
|
||||
$_SESSION[$cacheKey] = $data;
|
||||
$_SESSION[$timeKey] = time();
|
||||
|
||||
Feedback::flash('SUCCESS', 'DEFAULT', "Cache for agent {$agentId} is stored.");
|
||||
} elseif ($result === null && !empty($agent)) {
|
||||
unset($_SESSION["agent{$agent}_cache"]);
|
||||
unset($_SESSION["agent{$agent}_cache_time"]);
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => "Cache for agent {$agentId} is stored."
|
||||
'message' => "Cache for agent {$agent} is cleared."
|
||||
]);
|
||||
}
|
||||
else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid data format');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid data format']);
|
||||
}
|
||||
|
||||
// Handle AJAX requests
|
||||
} elseif (isset($_GET['action'])) {
|
||||
$action = $_GET['action'];
|
||||
$agentId = filter_input(INPUT_GET, 'agent', FILTER_VALIDATE_INT);
|
||||
|
||||
if ($action === 'fetch') {
|
||||
$response = ['status' => 'success', 'data' => $data];
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'status') {
|
||||
$response = ['status' => 'success', 'data' => $statusData];
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle template display
|
||||
} else {
|
||||
|
||||
// Validate platform_id is set
|
||||
if (!isset($platform_id)) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Platform ID is not set');
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Invalid data'
|
||||
]);
|
||||
}
|
||||
|
||||
// Get host details for this platform
|
||||
$hostDetails = $hostObject->getHostDetails($platform_id);
|
||||
//// if it's a GET request, it's read/load from cache
|
||||
//} elseif ($loadcache === true) {
|
||||
//
|
||||
// // check if cached data exists in session
|
||||
// if (isset($_SESSION["agent{$agent}_cache"])) {
|
||||
// // return the cached data in JSON format
|
||||
// echo json_encode(['status' => 'success', 'data' => $_SESSION["agent{$agent}_cache"]]);
|
||||
// } else {
|
||||
// // if no cached data exists
|
||||
// echo json_encode(['status' => 'error', 'message' => 'No cached data found']);
|
||||
// }
|
||||
|
||||
// Group agents by host
|
||||
$agentsByHost = [];
|
||||
foreach ($hostDetails as $host) {
|
||||
$hostId = $host['id'];
|
||||
$agentsByHost[$hostId] = [
|
||||
'host_name' => $host['name'],
|
||||
'agents' => []
|
||||
];
|
||||
|
||||
// Get agents for this host
|
||||
$hostAgents = $agentObject->getAgentDetails($hostId);
|
||||
if ($hostAgents) {
|
||||
$agentsByHost[$hostId]['agents'] = $hostAgents;
|
||||
// no form submitted, show the templates
|
||||
} else {
|
||||
$agentDetails = $agentObject->getAgentDetails($platform_id);
|
||||
include '../app/templates/agent-list.php';
|
||||
}
|
||||
|
||||
// Generate JWT tokens for each agent beforehand
|
||||
$agentTokens = [];
|
||||
foreach ($agentsByHost[$hostId]['agents'] as $agent) {
|
||||
$payload = [
|
||||
'iss' => 'Jilo Web',
|
||||
'aud' => $config['domain'],
|
||||
'iat' => time(),
|
||||
'exp' => time() + 3600,
|
||||
'agent_id' => $agent['id']
|
||||
];
|
||||
$agentTokens[$agent['id']] = $agentObject->generateAgentToken($payload, $agent['secret_key']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Now we have:
|
||||
* $hostDetails - hosts in this platform
|
||||
* $agentsByHost[$hostId]['agents'] - agents details by hostId
|
||||
* $agentTokens[$agent['id']] - tokens for the agentsIds
|
||||
*/
|
||||
}
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/agents.php';
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -8,46 +8,26 @@
|
|||
* Supports pagination.
|
||||
*/
|
||||
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
require '../app/classes/component.php';
|
||||
|
||||
// connect to database
|
||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
||||
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||
|
||||
// if DB connection has error, display it and stop here
|
||||
if ($response['db'] === null) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
Messages::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
|
||||
// otherwise if DB connection is OK, go on
|
||||
} else {
|
||||
$db = $response['db'];
|
||||
|
||||
// Get current page for pagination
|
||||
$currentPage = $_REQUEST['page_num'] ?? 1;
|
||||
$currentPage = (int)$currentPage;
|
||||
|
||||
// specify time range
|
||||
include '../app/helpers/time_range.php';
|
||||
|
||||
// Build params for pagination
|
||||
$params = '';
|
||||
if (!empty($_REQUEST['from_time'])) {
|
||||
$params .= '&from_time=' . urlencode($_REQUEST['from_time']);
|
||||
}
|
||||
if (!empty($_REQUEST['until_time'])) {
|
||||
$params .= '&until_time=' . urlencode($_REQUEST['until_time']);
|
||||
}
|
||||
if (!empty($_REQUEST['name'])) {
|
||||
$params .= '&name=' . urlencode($_REQUEST['name']);
|
||||
}
|
||||
if (!empty($_REQUEST['id'])) {
|
||||
$params .= '&id=' . urlencode($_REQUEST['id']);
|
||||
}
|
||||
if (isset($_REQUEST['event'])) {
|
||||
$params .= '&event=' . urlencode($_REQUEST['event']);
|
||||
}
|
||||
|
||||
// pagination variables
|
||||
$items_per_page = 20;
|
||||
$offset = ($currentPage -1) * $items_per_page;
|
||||
|
||||
// jitsi component events list
|
||||
// we use $_REQUEST, so that both links and forms work
|
||||
// if it's there, but empty, we make it same as the field name; otherwise assign the value
|
||||
|
@ -60,9 +40,15 @@ if ($response['db'] === null) {
|
|||
// Component events listings
|
||||
//
|
||||
|
||||
require '../app/classes/component.php';
|
||||
|
||||
// list of all component events (default)
|
||||
$componentObject = new Component($db);
|
||||
|
||||
// pagination variables
|
||||
$items_per_page = 15;
|
||||
$browse_page = $_REQUEST['p'] ?? 1;
|
||||
$browse_page = (int)$browse_page;
|
||||
$offset = ($browse_page -1) * $items_per_page;
|
||||
|
||||
// prepare the result
|
||||
$search = $componentObject->jitsiComponents($jitsi_component, $component_id, $event_type, $from_time, $until_time, $offset, $items_per_page);
|
||||
|
@ -71,7 +57,7 @@ if ($response['db'] === null) {
|
|||
if (!empty($search)) {
|
||||
// we get total items and number of pages
|
||||
$item_count = count($search_all);
|
||||
$totalPages = ceil($item_count / $items_per_page);
|
||||
$page_count = ceil($item_count / $items_per_page);
|
||||
|
||||
$components = array();
|
||||
$components['records'] = array();
|
||||
|
@ -92,18 +78,30 @@ if ($response['db'] === null) {
|
|||
}
|
||||
}
|
||||
|
||||
// filter message
|
||||
$filterMessage = array();
|
||||
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
|
||||
array_push($filterMessage, 'Jitsi events for component "<strong>' . $_REQUEST['name'] . '</strong>"');
|
||||
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
|
||||
array_push($filterMessage, 'Jitsi events for component ID "<strong>' . $_REQUEST['id'] . '</strong>"');
|
||||
}
|
||||
// prepare the widget
|
||||
$widget['full'] = false;
|
||||
$widget['name'] = 'AllComponents';
|
||||
$widget['filter'] = true;
|
||||
$widget['pagination'] = true;
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// widget title
|
||||
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
|
||||
$widget['title'] = 'Jitsi events for component <strong>' . $_REQUEST['name'] . '</strong>';
|
||||
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
|
||||
$widget['title'] = 'Jitsi events for component ID <strong>' . $_REQUEST['id'] . '</strong>';
|
||||
} else {
|
||||
$widget['title'] = 'Jitsi events for <strong>all components</strong>';
|
||||
}
|
||||
// widget records
|
||||
if (!empty($components['records'])) {
|
||||
$widget['full'] = true;
|
||||
$widget['table_headers'] = array_keys($components['records'][0]);
|
||||
$widget['table_records'] = $components['records'];
|
||||
}
|
||||
|
||||
// display the widget
|
||||
include '../app/templates/components.php';
|
||||
include '../app/templates/event-list-components.php';
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -8,12 +8,18 @@
|
|||
* Supports pagination.
|
||||
*/
|
||||
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
require '../app/classes/conference.php';
|
||||
|
||||
// connect to database
|
||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
||||
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||
|
||||
// if DB connection has error, display it and stop here
|
||||
if ($response['db'] === null) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
Messages::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
|
||||
// otherwise if DB connection is OK, go on
|
||||
} else {
|
||||
|
@ -46,31 +52,13 @@ if ($response['db'] === null) {
|
|||
// Conference listings
|
||||
//
|
||||
|
||||
require '../app/classes/conference.php';
|
||||
$conferenceObject = new Conference($db);
|
||||
|
||||
// get current page for pagination
|
||||
$currentPage = $_REQUEST['page_num'] ?? 1;
|
||||
$currentPage = (int)$currentPage;
|
||||
|
||||
// pagination variables
|
||||
$items_per_page = 20;
|
||||
$offset = ($currentPage -1) * $items_per_page;
|
||||
|
||||
// Build params for pagination
|
||||
$params = '';
|
||||
if (!empty($_REQUEST['from_time'])) {
|
||||
$params .= '&from_time=' . urlencode($_REQUEST['from_time']);
|
||||
}
|
||||
if (!empty($_REQUEST['until_time'])) {
|
||||
$params .= '&until_time=' . urlencode($_REQUEST['until_time']);
|
||||
}
|
||||
if (!empty($_REQUEST['name'])) {
|
||||
$params .= '&name=' . urlencode($_REQUEST['name']);
|
||||
}
|
||||
if (!empty($_REQUEST['id'])) {
|
||||
$params .= '&id=' . urlencode($_REQUEST['id']);
|
||||
}
|
||||
$items_per_page = 15;
|
||||
$browse_page = $_REQUEST['p'] ?? 1;
|
||||
$browse_page = (int)$browse_page;
|
||||
$offset = ($browse_page -1) * $items_per_page;
|
||||
|
||||
// search and list specific conference ID
|
||||
if (isset($conferenceId)) {
|
||||
|
@ -89,7 +77,7 @@ if ($response['db'] === null) {
|
|||
if (!empty($search)) {
|
||||
// we get total items and number of pages
|
||||
$item_count = count($search_all);
|
||||
$totalPages = ceil($item_count / $items_per_page);
|
||||
$page_count = ceil($item_count / $items_per_page);
|
||||
|
||||
$conferences = array();
|
||||
$conferences['records'] = array();
|
||||
|
@ -151,18 +139,32 @@ if ($response['db'] === null) {
|
|||
}
|
||||
}
|
||||
|
||||
// filter message
|
||||
$filterMessage = array();
|
||||
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
|
||||
array_push($filterMessage, 'Conferences with name matching "<strong>' . $_REQUEST['name'] . '</strong>"');
|
||||
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
|
||||
array_push($filterMessage, 'Conference with ID "<strong>' . $_REQUEST['id'] . '</strong>"');
|
||||
}
|
||||
// prepare the widget
|
||||
$widget['full'] = false;
|
||||
$widget['name'] = 'Conferences';
|
||||
$widget['collapsible'] = false;
|
||||
$widget['collapsed'] = false;
|
||||
$widget['filter'] = true;
|
||||
$widget['pagination'] = true;
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// widget title
|
||||
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
|
||||
$widget['title'] = 'Conferences with name matching "<strong>' . $_REQUEST['name'] . '"</strong>';
|
||||
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
|
||||
$widget['title'] = 'Conference with ID "<strong>' . $_REQUEST['id'] . '"</strong>';
|
||||
} else {
|
||||
$widget['title'] = 'All conferences';
|
||||
}
|
||||
// widget records
|
||||
if (!empty($conferences['records'])) {
|
||||
$widget['full'] = true;
|
||||
$widget['table_headers'] = array_keys($conferences['records'][0]);
|
||||
$widget['table_records'] = $conferences['records'];
|
||||
}
|
||||
|
||||
// display the widget
|
||||
include '../app/templates/conferences.php';
|
||||
include '../app/templates/event-list-conferences.php';
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,124 +1,257 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Config management.
|
||||
* Configuration management.
|
||||
*
|
||||
* This page handles the config file.
|
||||
* This page ("config") handles configuration by adding, editing, and deleting platforms,
|
||||
* hosts, agents, and the configuration file itself.
|
||||
*/
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$agent = $_REQUEST['agent'] ?? '';
|
||||
$host = $_REQUEST['host'] ?? '';
|
||||
|
||||
require '../app/classes/config.php';
|
||||
require '../app/classes/api_response.php';
|
||||
require '../app/classes/host.php';
|
||||
require '../app/classes/agent.php';
|
||||
|
||||
// Initialize required objects
|
||||
$userObject = new User($db);
|
||||
$configObject = new Config();
|
||||
$hostObject = new Host($dbWeb);
|
||||
$agentObject = new Agent($dbWeb);
|
||||
|
||||
// For AJAX requests
|
||||
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
/**
|
||||
* Handles form submissions from editing page
|
||||
*/
|
||||
|
||||
// Set JSON content type for AJAX requests
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
}
|
||||
|
||||
// Ensure config file path is set
|
||||
if (!isset($config_file) || empty($config_file)) {
|
||||
if ($isAjax) {
|
||||
ApiResponse::error('Config file path not set');
|
||||
exit;
|
||||
// editing the config file
|
||||
if (isset($_POST['item']) && $_POST['item'] === 'config_file') {
|
||||
// check if file is writable
|
||||
if (!is_writable($config_file)) {
|
||||
$_SESSION['error'] = "Configuration file is not writable.";
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Config file path not set');
|
||||
header('Location: ' . htmlspecialchars($app_root));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if file is writable
|
||||
$isWritable = is_writable($config_file);
|
||||
$configMessage = '';
|
||||
if (!$isWritable) {
|
||||
$configMessage = Feedback::render('ERROR', 'DEFAULT', 'Config file is not writable', false);
|
||||
if ($isAjax) {
|
||||
ApiResponse::error('Config file is not writable', null, 403);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Check if user has permission to edit config
|
||||
if (!$userObject->hasRight($userId, 'edit config file')) {
|
||||
$logObject->log('error', "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", ['user_id' => $userId, 'scope' => 'system']);
|
||||
if ($isAjax) {
|
||||
ApiResponse::error('Forbidden: You do not have permission to edit the config file', null, 403);
|
||||
exit;
|
||||
} else {
|
||||
include '../app/templates/error-unauthorized.php';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply rate limiting
|
||||
require '../app/includes/rate_limit_middleware.php';
|
||||
checkRateLimit($db, 'config', $userId);
|
||||
|
||||
// Ensure no output before this point
|
||||
ob_clean();
|
||||
|
||||
// For AJAX requests, get JSON data
|
||||
if ($isAjax) {
|
||||
// Get raw input
|
||||
$jsonData = file_get_contents('php://input');
|
||||
if ($jsonData === false) {
|
||||
$logObject->log('error', "Failed to read request data for config update", ['user_id' => $userId, 'scope' => 'system']);
|
||||
ApiResponse::error('Failed to read request data');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Try to parse JSON
|
||||
$postData = json_decode($jsonData, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$error = json_last_error_msg();
|
||||
ApiResponse::error('Invalid JSON data received: ' . $error);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Try to update config file
|
||||
$result = $configObject->editConfigFile($postData, $config_file);
|
||||
if ($result['success']) {
|
||||
ApiResponse::success($result['updated'], 'Config file updated successfully');
|
||||
} else {
|
||||
ApiResponse::error($result['error']);
|
||||
}
|
||||
exit;
|
||||
} else {
|
||||
// Handle non-AJAX POST
|
||||
$result = $configObject->editConfigFile($_POST, $config_file);
|
||||
if ($result['success']) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', 'Config file updated successfully', true);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "The config file is edited.";
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Error updating config file: " . $result['error'], true);
|
||||
}
|
||||
|
||||
header('Location: ' . htmlspecialchars($app_root) . '?page=config');
|
||||
exit;
|
||||
$_SESSION['error'] = "Editing the config file failed. Error: $result";
|
||||
}
|
||||
}
|
||||
|
||||
// Only include template for non-AJAX requests
|
||||
if (!$isAjax) {
|
||||
// new host adding
|
||||
} elseif (isset($_POST['new']) && isset($_POST['item']) && $_POST['new'] === 'true' && $_POST['item'] === 'host') {
|
||||
$newHost = [
|
||||
'address' => $address,
|
||||
'port' => $port,
|
||||
'platform_id' => $platform_id,
|
||||
'name' => $name,
|
||||
];
|
||||
$result = $hostObject->addHost($newHost);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "New Jilo host added.";
|
||||
} else {
|
||||
$_SESSION['error'] = "Adding the host failed. Error: $result";
|
||||
}
|
||||
|
||||
// new agent adding
|
||||
} elseif (isset($_POST['new']) && isset($_POST['item']) && $_POST['new'] === 'true' && $_POST['item'] === 'agent') {
|
||||
$newAgent = [
|
||||
'type_id' => $type,
|
||||
'url' => $url,
|
||||
'secret_key' => $secret_key,
|
||||
'check_period' => $check_period,
|
||||
];
|
||||
$result = $agentObject->addAgent($platform_id, $newAgent);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "New Jilo Agent added.";
|
||||
} else {
|
||||
$_SESSION['error'] = "Adding the agent failed. Error: $result";
|
||||
}
|
||||
|
||||
// new platform adding
|
||||
} elseif (isset($_POST['new']) && $_POST['new'] === 'true') {
|
||||
$newPlatform = [
|
||||
'name' => $name,
|
||||
'jitsi_url' => $_POST['jitsi_url'],
|
||||
'jilo_database' => $_POST['jilo_database'],
|
||||
];
|
||||
$result = $platformObject->addPlatform($newPlatform);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "New Jitsi platform added.";
|
||||
} else {
|
||||
$_SESSION['error'] = "Adding the platform failed. Error: $result";
|
||||
}
|
||||
|
||||
// deleting a host
|
||||
} elseif (isset($_POST['delete']) && isset($_POST['host']) && $_POST['delete'] === 'true') {
|
||||
$result = $hostObject->deleteHost($host);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "Host id \"{$_REQUEST['host']}\" deleted.";
|
||||
} else {
|
||||
$_SESSION['error'] = "Deleting the host failed. Error: $result";
|
||||
}
|
||||
|
||||
// deleting an agent
|
||||
} elseif (isset($_POST['delete']) && isset($_POST['agent']) && $_POST['delete'] === 'true') {
|
||||
$result = $agentObject->deleteAgent($agent);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "Agent id \"{$_REQUEST['agent']}\" deleted.";
|
||||
} else {
|
||||
$_SESSION['error'] = "Deleting the agent failed. Error: $result";
|
||||
}
|
||||
|
||||
// deleting a platform
|
||||
} elseif (isset($_POST['delete']) && $_POST['delete'] === 'true') {
|
||||
$platform = $_POST['platform'];
|
||||
$result = $platformObject->deletePlatform($platform);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "Platform \"{$platformObject['name']}\" added.";
|
||||
} else {
|
||||
$_SESSION['error'] = "Adding the platform failed. Error: $result";
|
||||
}
|
||||
|
||||
// an update to an existing host
|
||||
} elseif (isset($_POST['host'])) {
|
||||
$updatedHost = [
|
||||
'id' => $host,
|
||||
'address' => $address,
|
||||
'port' => $port,
|
||||
'name' => $name,
|
||||
];
|
||||
$result = $hostObject->editHost($platform_id, $updatedHost);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "Host \"{$_REQUEST['address']}:{$_REQUEST['port']}\" edited.";
|
||||
} else {
|
||||
$_SESSION['error'] = "Editing the host failed. Error: $result";
|
||||
}
|
||||
|
||||
// an update to an existing agent
|
||||
} elseif (isset($_POST['agent'])) {
|
||||
$updatedAgent = [
|
||||
'id' => $agent,
|
||||
'agent_type_id' => $type,
|
||||
'url' => $url,
|
||||
'secret_key' => $secret_key,
|
||||
'check_period' => $check_period,
|
||||
];
|
||||
$result = $agentObject->editAgent($platform_id, $updatedAgent);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "Agent id \"{$_REQUEST['agent']}\" edited.";
|
||||
} else {
|
||||
$_SESSION['error'] = "Editing the agent failed. Error: $result";
|
||||
}
|
||||
|
||||
// an update to an existing platform
|
||||
} else {
|
||||
$platform = $_POST['platform'];
|
||||
$updatedPlatform = [
|
||||
'name' => $name,
|
||||
'jitsi_url' => $_POST['jitsi_url'],
|
||||
'jilo_database' => $_POST['jilo_database'],
|
||||
];
|
||||
$result = $platformObject->editPlatform($platform, $updatedPlatform);
|
||||
if ($result === true) {
|
||||
$_SESSION['notice'] = "Platform \"{$_REQUEST['name']}\" edited.";
|
||||
} else {
|
||||
$_SESSION['error'] = "Editing the platform failed. Error: $result";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// FIXME the new file is not loaded on first page load
|
||||
unset($config);
|
||||
header("Location: $app_root?page=config&item=$item");
|
||||
exit();
|
||||
|
||||
} else {
|
||||
/**
|
||||
* Handles GET requests to display templates.
|
||||
*/
|
||||
|
||||
if ($userObject->hasRight($userId, 'superuser') ||
|
||||
$userObject->hasRight($userId, 'view config file')) {
|
||||
include '../app/templates/config.php';
|
||||
switch ($item) {
|
||||
|
||||
case 'platform':
|
||||
if (isset($action) && $action === 'add') {
|
||||
include '../app/templates/config-platform-add.php';
|
||||
} elseif (isset($action) && $action === 'edit') {
|
||||
include '../app/templates/config-platform-edit.php';
|
||||
} elseif (isset($action) && $action === 'delete') {
|
||||
include '../app/templates/config-platform-delete.php';
|
||||
} else {
|
||||
if ($userObject->hasRight($user_id, 'view config file')) {
|
||||
include '../app/templates/config-platform.php';
|
||||
} else {
|
||||
$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';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'host':
|
||||
if (isset($action) && $action === 'add') {
|
||||
include '../app/templates/config-host-add.php';
|
||||
} elseif (isset($action) && $action === 'edit') {
|
||||
$hostDetails = $hostObject->getHostDetails($platform_id, $agent);
|
||||
include '../app/templates/config-host-edit.php';
|
||||
} elseif (isset($action) && $action === 'delete') {
|
||||
$hostDetails = $hostObject->getHostDetails($platform_id, $agent);
|
||||
include '../app/templates/config-host-delete.php';
|
||||
} else {
|
||||
if ($userObject->hasRight($user_id, 'view config file')) {
|
||||
$hostDetails = $hostObject->getHostDetails();
|
||||
include '../app/templates/config-host.php';
|
||||
} else {
|
||||
include '../app/templates/error-unauthorized.php';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'endpoint':
|
||||
// TODO ad here endpoints options
|
||||
echo 'under construction';
|
||||
// switch ($action) {
|
||||
// case 'add-agent':
|
||||
// $jilo_agent_types = $agentObject->getAgentTypes();
|
||||
// $jilo_agents_in_platform = $agentObject->getPlatformAgentTypes($platform_id);
|
||||
// $jilo_agent_types_in_platform = array_column($jilo_agents_in_platform, 'agent_type_id');
|
||||
// include '../app/templates/config-add-agent.php';
|
||||
// break;
|
||||
// case 'edit':
|
||||
// if (isset($_GET['agent'])) {
|
||||
// $agentDetails = $agentObject->getAgentDetails($platform_id, $agent);
|
||||
// $jilo_agent_types = $agentObject->getAgentTypes();
|
||||
// include '../app/templates/config-edit-agent.php';
|
||||
// }
|
||||
// break;
|
||||
// case 'delete':
|
||||
// if (isset($_GET['agent'])) {
|
||||
// $agentDetails = $agentObject->getAgentDetails($platform_id, $agent);
|
||||
// include '../app/templates/config-delete-agent.php';
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
break;
|
||||
|
||||
case 'config_file':
|
||||
if (isset($action) && $action === 'edit') {
|
||||
include '../app/templates/config-configfile-edit.php';
|
||||
} else {
|
||||
if ($userObject->hasRight($user_id, 'view config file')) {
|
||||
include '../app/templates/config-configfile.php';
|
||||
} else {
|
||||
include '../app/templates/error-unauthorized.php';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// the default config page is the platforms page
|
||||
header("Location: $app_root?page=config&item=platform");
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* User credentials management
|
||||
*
|
||||
* This page ("credentials") handles all credential-related actions including:
|
||||
* - Two-factor authentication (2FA) setup, verification, and management
|
||||
* - Password changes and resets
|
||||
*
|
||||
* Actions handled:
|
||||
* - `setup`: Initial 2FA setup and verification
|
||||
* - `verify`: Verify 2FA codes during login
|
||||
* - `disable`: Disable 2FA
|
||||
* - `password`: Change password
|
||||
*/
|
||||
|
||||
// Initialize user object
|
||||
$userObject = new User($db);
|
||||
|
||||
// Get action and item from request
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$item = $_REQUEST['item'] ?? '';
|
||||
|
||||
// if a form is submitted
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
// Validate CSRF token
|
||||
$security->verifyCsrfToken($_POST['csrf_token'] ?? '');
|
||||
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid security token. Please try again.');
|
||||
header("Location: $app_root?page=credentials");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Apply rate limiting
|
||||
require_once '../app/includes/rate_limit_middleware.php';
|
||||
checkRateLimit($db, 'credentials', $userId);
|
||||
|
||||
switch ($item) {
|
||||
case '2fa':
|
||||
switch ($action) {
|
||||
case 'setup':
|
||||
// Validate the setup code
|
||||
$code = $_POST['code'] ?? '';
|
||||
$secret = $_POST['secret'] ?? '';
|
||||
|
||||
if ($userObject->enableTwoFactor($userId, $secret, $code)) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been enabled successfully.');
|
||||
header("Location: $app_root?page=credentials");
|
||||
exit();
|
||||
} else {
|
||||
// Only show error if code was actually submitted
|
||||
if ($code !== '') {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code. Please try again.');
|
||||
}
|
||||
header("Location: $app_root?page=credentials&action=setup");
|
||||
exit();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'verify':
|
||||
// This is a user-initiated verification
|
||||
$code = $_POST['code'] ?? '';
|
||||
if ($userObject->verifyTwoFactor($userId, $code)) {
|
||||
$_SESSION['2fa_verified'] = true;
|
||||
header("Location: $app_root?page=dashboard");
|
||||
exit();
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code. Please try again.');
|
||||
header("Location: $app_root?page=credentials&action=verify");
|
||||
exit();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'disable':
|
||||
if ($userObject->disableTwoFactor($userId)) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been disabled.');
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable two-factor authentication.');
|
||||
}
|
||||
header("Location: $app_root?page=credentials");
|
||||
exit();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'password':
|
||||
require_once '../app/classes/validator.php';
|
||||
|
||||
$validator = new Validator($_POST);
|
||||
$rules = [
|
||||
'current_password' => [
|
||||
'required' => true
|
||||
],
|
||||
'new_password' => [
|
||||
'required' => true,
|
||||
'min' => 8
|
||||
],
|
||||
'confirm_password' => [
|
||||
'required' => true,
|
||||
'matches' => 'new_password'
|
||||
]
|
||||
];
|
||||
|
||||
if (!$validator->validate($rules)) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
|
||||
header("Location: $app_root?page=credentials");
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($userObject->changePassword($userId, $_POST['current_password'], $_POST['new_password'])) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', 'Password has been changed successfully.');
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Failed to change password. Please verify your current password.');
|
||||
}
|
||||
header("Location: $app_root?page=credentials");
|
||||
exit();
|
||||
break;
|
||||
}
|
||||
|
||||
// no form submitted, show the templates
|
||||
} else {
|
||||
// Get user timezone for templates
|
||||
$userTimezone = !empty($userDetails[0]['timezone']) ? $userDetails[0]['timezone'] : 'UTC';
|
||||
|
||||
// Generate CSRF token if not exists
|
||||
require_once '../app/helpers/security.php';
|
||||
$security = SecurityHelper::getInstance();
|
||||
$security->generateCsrfToken();
|
||||
|
||||
// Get 2FA status for the template
|
||||
$has2fa = $userObject->isTwoFactorEnabled($userId);
|
||||
|
||||
switch ($action) {
|
||||
case 'setup':
|
||||
if (!$has2fa) {
|
||||
$result = $userObject->enableTwoFactor($userId);
|
||||
if ($result['success']) {
|
||||
$setupData = $result['data'];
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $result['message'] ?? 'Failed to generate 2FA setup data');
|
||||
header("Location: $app_root?page=credentials");
|
||||
exit();
|
||||
}
|
||||
}
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the 2FA setup template
|
||||
include '../app/templates/credentials-2fa-setup.php';
|
||||
break;
|
||||
|
||||
case 'verify':
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the 2FA verification template
|
||||
include '../app/templates/credentials-2fa-verify.php';
|
||||
break;
|
||||
|
||||
default:
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the combined management template
|
||||
include '../app/templates/credentials-manage.php';
|
||||
}
|
||||
}
|
|
@ -9,18 +9,19 @@
|
|||
* 3. The most recent 10 conferences.
|
||||
*/
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
require '../app/classes/conference.php';
|
||||
require '../app/classes/participant.php';
|
||||
|
||||
// connect to database
|
||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
||||
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||
|
||||
// if DB connection has error, display it and stop here
|
||||
if ($response['db'] === null) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
Messages::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
|
||||
// otherwise if DB connection is OK, go on
|
||||
} else {
|
||||
|
@ -227,3 +228,5 @@ if ($response['db'] === null) {
|
|||
include '../app/templates/widget.php';
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$agent = $_REQUEST['agent'] ?? '';
|
||||
|
||||
require '../app/classes/config.php';
|
||||
require '../app/classes/agent.php';
|
||||
require '../app/classes/conference.php';
|
||||
|
||||
$configObject = new Config();
|
||||
$agentObject = new Agent($dbWeb);
|
||||
|
||||
// connect to Jilo database
|
||||
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||
|
||||
// if DB connection has error, display it and stop here
|
||||
if ($response['db'] === null) {
|
||||
Messages::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
|
||||
// otherwise if DB connection is OK, go on
|
||||
} else {
|
||||
$db = $response['db'];
|
||||
|
||||
$conferenceObject = new Conference($db);
|
||||
|
||||
switch ($item) {
|
||||
|
||||
case 'graphs':
|
||||
// Connect to Jilo database for log data
|
||||
$jilo_response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||
if ($jilo_response['db'] === null) {
|
||||
Messages::flash('ERROR', 'DEFAULT', $jilo_response['error']);
|
||||
break;
|
||||
}
|
||||
$jilo_db = $jilo_response['db'];
|
||||
|
||||
// Get date range for the last 7 days
|
||||
$from_time = date('Y-m-d', strtotime('-7 days'));
|
||||
$until_time = date('Y-m-d');
|
||||
|
||||
// Define graphs to show
|
||||
$graphs = [
|
||||
[
|
||||
'graph_name' => 'conferences',
|
||||
'graph_title' => 'Conferences in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time',
|
||||
'datasets' => []
|
||||
],
|
||||
[
|
||||
'graph_name' => 'participants',
|
||||
'graph_title' => 'Participants in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time',
|
||||
'datasets' => []
|
||||
]
|
||||
];
|
||||
|
||||
// Get Jitsi API data
|
||||
$conferences_api = $agentObject->getHistoricalData(
|
||||
$platform_id,
|
||||
'jicofo',
|
||||
'conferences',
|
||||
$from_time,
|
||||
$until_time
|
||||
);
|
||||
$graphs[0]['datasets'][] = [
|
||||
'data' => $conferences_api,
|
||||
'label' => 'Conferences from Jitsi API',
|
||||
'color' => 'rgba(75, 192, 192, 1)'
|
||||
];
|
||||
|
||||
// Get conference data from logs
|
||||
$conferences_logs = $conferenceObject->conferenceNumber(
|
||||
$from_time,
|
||||
$until_time
|
||||
);
|
||||
$graphs[0]['datasets'][] = [
|
||||
'data' => $conferences_logs,
|
||||
'label' => 'Conferences from Logs',
|
||||
'color' => 'rgba(255, 99, 132, 1)'
|
||||
];
|
||||
|
||||
// Get participants data
|
||||
$participants_api = $agentObject->getHistoricalData(
|
||||
$platform_id,
|
||||
'jicofo',
|
||||
'participants',
|
||||
$from_time,
|
||||
$until_time
|
||||
);
|
||||
$graphs[1]['datasets'][] = [
|
||||
'data' => $participants_api,
|
||||
'label' => 'Participants from Jitsi API',
|
||||
'color' => 'rgba(75, 192, 192, 1)'
|
||||
];
|
||||
|
||||
// Prepare data for template
|
||||
$graph = $graphs;
|
||||
|
||||
// prepare the widget
|
||||
$widget['full'] = false;
|
||||
$widget['name'] = 'Graphs';
|
||||
$widget['title'] = 'Jitsi graphs';
|
||||
|
||||
include '../app/templates/graphs-combined.php';
|
||||
break;
|
||||
|
||||
case 'latest':
|
||||
// Define metrics to display
|
||||
$metrics = [
|
||||
'Basic stats' => [
|
||||
'conferences' => ['label' => 'Current conferences', 'link' => 'conferences'],
|
||||
'participants' => ['label' => 'Current participants', 'link' => 'participants'],
|
||||
'total_conferences_created' => ['label' => 'Total conferences created'],
|
||||
'total_participants' => ['label' => 'Total participants']
|
||||
],
|
||||
'Bridge stats' => [
|
||||
'bridge_selector.bridge_count' => ['label' => 'Bridge count'],
|
||||
'bridge_selector.operational_bridge_count' => ['label' => 'Operational bridges'],
|
||||
'bridge_selector.in_shutdown_bridge_count' => ['label' => 'Bridges in shutdown']
|
||||
],
|
||||
'Jibri stats' => [
|
||||
'jibri_detector.count' => ['label' => 'Jibri count'],
|
||||
'jibri_detector.available' => ['label' => 'Jibri idle'],
|
||||
'jibri.live_streaming_active' => ['label' => 'Jibri active streaming'],
|
||||
'jibri.recording_active' => ['label' => 'Jibri active recording'],
|
||||
|
||||
],
|
||||
'System stats' => [
|
||||
'threads' => ['label' => 'Threads'],
|
||||
'stress_level' => ['label' => 'Stress level'],
|
||||
'version' => ['label' => 'Version']
|
||||
]
|
||||
];
|
||||
|
||||
// Get latest data for all the agents
|
||||
$agents = ['jvb', 'jicofo', 'jibri', 'prosody', 'nginx'];
|
||||
$widget['records'] = [];
|
||||
|
||||
// Initialize records for each agent
|
||||
foreach ($agents as $agent) {
|
||||
$record = [
|
||||
'table_headers' => strtoupper($agent),
|
||||
'metrics' => [],
|
||||
'timestamp' => null
|
||||
];
|
||||
|
||||
// Fetch all metrics for this agent
|
||||
foreach ($metrics as $section => $section_metrics) {
|
||||
foreach ($section_metrics as $metric => $metricConfig) {
|
||||
$data = $agentObject->getLatestData($platform_id, $agent, $metric);
|
||||
if ($data !== null) {
|
||||
$record['metrics'][$section][$metric] = [
|
||||
'value' => $data['value'],
|
||||
'label' => $metricConfig['label'],
|
||||
'link' => isset($metricConfig['link']) ? $metricConfig['link'] : null
|
||||
];
|
||||
// Use the most recent timestamp
|
||||
if ($record['timestamp'] === null || strtotime($data['timestamp']) > strtotime($record['timestamp'])) {
|
||||
$record['timestamp'] = $data['timestamp'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($record['metrics'])) {
|
||||
$widget['records'][] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
// prepare the widget
|
||||
$widget['full'] = false;
|
||||
$widget['name'] = 'LatestData';
|
||||
$widget['title'] = 'Latest data from Jilo Agents';
|
||||
$widget['collapsible'] = false;
|
||||
$widget['collapsed'] = false;
|
||||
$widget['filter'] = false;
|
||||
$widget['metrics'] = $metrics; // Pass metrics configuration to template
|
||||
if (!empty($widget['records'])) {
|
||||
$widget['full'] = true;
|
||||
}
|
||||
$widget['pagination'] = false;
|
||||
|
||||
include '../app/templates/latest-data.php';
|
||||
break;
|
||||
|
||||
case 'configjs':
|
||||
$mode = $_REQUEST['mode'] ?? '';
|
||||
$raw = ($mode === 'raw');
|
||||
$platformConfigjs = $configObject->getPlatformConfigjs($platformDetails[0]['jitsi_url'], $raw);
|
||||
include '../app/templates/data-configjs.php';
|
||||
break;
|
||||
|
||||
case 'interfaceconfigjs':
|
||||
$mode = $_REQUEST['mode'] ?? '';
|
||||
$raw = ($mode === 'raw');
|
||||
$platformInterfaceConfigjs = $configObject->getPlatformInterfaceConfigjs($platformDetails[0]['jitsi_url'], $raw);
|
||||
include '../app/templates/data-interfaceconfigjs.php';
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,91 +0,0 @@
|
|||
<?php
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$agent = $_REQUEST['agent'] ?? '';
|
||||
|
||||
require '../app/classes/agent.php';
|
||||
require '../app/classes/conference.php';
|
||||
require '../app/classes/host.php';
|
||||
|
||||
$agentObject = new Agent($db);
|
||||
$hostObject = new Host($db);
|
||||
|
||||
// Connect to Jilo database for log data
|
||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
||||
if ($response['db'] === null) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
} else {
|
||||
$db = $response['db'];
|
||||
}
|
||||
$conferenceObject = new Conference($db);
|
||||
|
||||
// Get date range for the last 7 days
|
||||
$from_time = date('Y-m-d', strtotime('-7 days'));
|
||||
$until_time = date('Y-m-d');
|
||||
|
||||
// Define graphs to show
|
||||
$graphs = [
|
||||
[
|
||||
'graph_name' => 'conferences',
|
||||
'graph_title' => 'Conferences in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time',
|
||||
'datasets' => []
|
||||
],
|
||||
[
|
||||
'graph_name' => 'participants',
|
||||
'graph_title' => 'Participants in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time',
|
||||
'datasets' => []
|
||||
]
|
||||
];
|
||||
|
||||
// Get Jitsi API data
|
||||
$conferences_api = $agentObject->getHistoricalData(
|
||||
$platform_id,
|
||||
'jicofo',
|
||||
'conferences',
|
||||
$from_time,
|
||||
$until_time
|
||||
);
|
||||
$graphs[0]['datasets'][] = [
|
||||
'data' => $conferences_api,
|
||||
'label' => 'Conferences from Jitsi API',
|
||||
'color' => 'rgba(75, 192, 192, 1)'
|
||||
];
|
||||
|
||||
// Get conference data from logs
|
||||
$conferences_logs = $conferenceObject->conferenceNumber(
|
||||
$from_time,
|
||||
$until_time
|
||||
);
|
||||
$graphs[0]['datasets'][] = [
|
||||
'data' => $conferences_logs,
|
||||
'label' => 'Conferences from Logs',
|
||||
'color' => 'rgba(255, 99, 132, 1)'
|
||||
];
|
||||
|
||||
// Get participants data
|
||||
$participants_api = $agentObject->getHistoricalData(
|
||||
$platform_id,
|
||||
'jicofo',
|
||||
'participants',
|
||||
$from_time,
|
||||
$until_time
|
||||
);
|
||||
$graphs[1]['datasets'][] = [
|
||||
'data' => $participants_api,
|
||||
'label' => 'Participants from Jitsi API',
|
||||
'color' => 'rgba(75, 192, 192, 1)'
|
||||
];
|
||||
|
||||
// Prepare data for template
|
||||
$graph = $graphs;
|
||||
|
||||
// prepare the widget
|
||||
$widget['full'] = false;
|
||||
$widget['name'] = 'Graphs';
|
||||
$widget['title'] = 'Jitsi graphs';
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/graphs.php';
|
|
@ -1,6 +1,9 @@
|
|||
<?php
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
include '../app/templates/help.php';
|
||||
include '../app/templates/help-main.php';
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
<?php
|
||||
|
||||
require '../app/classes/agent.php';
|
||||
require '../app/classes/host.php';
|
||||
|
||||
$agentObject = new Agent($db);
|
||||
$hostObject = new Host($db);
|
||||
|
||||
// Define metrics to display
|
||||
$metrics = [
|
||||
'Basic stats' => [
|
||||
'conferences' => ['label' => 'Current conferences', 'link' => 'conferences'],
|
||||
'participants' => ['label' => 'Current participants', 'link' => 'participants'],
|
||||
'total_conferences_created' => ['label' => 'Total conferences created'],
|
||||
'total_participants' => ['label' => 'Total participants']
|
||||
],
|
||||
'Bridge stats' => [
|
||||
'bridge_selector.bridge_count' => ['label' => 'Bridge count'],
|
||||
'bridge_selector.operational_bridge_count' => ['label' => 'Operational bridges'],
|
||||
'bridge_selector.in_shutdown_bridge_count' => ['label' => 'Bridges in shutdown']
|
||||
],
|
||||
'Jibri stats' => [
|
||||
'jibri_detector.count' => ['label' => 'Jibri count'],
|
||||
'jibri_detector.available' => ['label' => 'Jibri idle'],
|
||||
'jibri.live_streaming_active' => ['label' => 'Jibri active streaming'],
|
||||
'jibri.recording_active' => ['label' => 'Jibri active recording'],
|
||||
],
|
||||
'System stats' => [
|
||||
'threads' => ['label' => 'Threads'],
|
||||
'stress_level' => ['label' => 'Stress level'],
|
||||
'version' => ['label' => 'Version']
|
||||
]
|
||||
];
|
||||
|
||||
// Get all hosts for this platform
|
||||
$hosts = $hostObject->getHostDetails($platform_id);
|
||||
$hostsData = [];
|
||||
|
||||
// For each host, get its agents and their metrics
|
||||
foreach ($hosts as $host) {
|
||||
$hostData = [
|
||||
'id' => $host['id'],
|
||||
'name' => $host['name'] ?: $host['address'],
|
||||
'address' => $host['address'],
|
||||
'agents' => []
|
||||
];
|
||||
|
||||
// Get agents for this host
|
||||
$hostAgents = $agentObject->getAgentDetails($host['id']);
|
||||
foreach ($hostAgents as $agent) {
|
||||
$agentData = [
|
||||
'id' => $agent['id'],
|
||||
'type' => $agent['agent_description'],
|
||||
'name' => strtoupper($agent['agent_description']),
|
||||
'metrics' => [],
|
||||
'timestamp' => null
|
||||
];
|
||||
|
||||
// Fetch all metrics for this agent
|
||||
foreach ($metrics as $section => $section_metrics) {
|
||||
foreach ($section_metrics as $metric => $metricConfig) {
|
||||
// Get latest data
|
||||
$latestData = $agentObject->getLatestData($host['id'], $agent['agent_description'], $metric);
|
||||
|
||||
if ($latestData !== null) {
|
||||
// Get the previous record
|
||||
$previousData = $agentObject->getPreviousRecord(
|
||||
$host['id'],
|
||||
$agent['agent_description'],
|
||||
$metric,
|
||||
$latestData['timestamp']
|
||||
);
|
||||
|
||||
$agentData['metrics'][$section][$metric] = [
|
||||
'latest' => [
|
||||
'value' => $latestData['value'],
|
||||
'timestamp' => $latestData['timestamp']
|
||||
],
|
||||
'previous' => $previousData,
|
||||
'label' => $metricConfig['label'],
|
||||
'link' => isset($metricConfig['link']) ? $metricConfig['link'] : null
|
||||
];
|
||||
|
||||
// Use the most recent timestamp for the agent
|
||||
if ($agentData['timestamp'] === null || strtotime($latestData['timestamp']) > strtotime($agentData['timestamp'])) {
|
||||
$agentData['timestamp'] = $latestData['timestamp'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($agentData['metrics'])) {
|
||||
$hostData['agents'][] = $agentData;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($hostData['agents'])) {
|
||||
$hostsData[] = $hostData;
|
||||
}
|
||||
}
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/latest.php';
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
$mode = $_REQUEST['mode'] ?? '';
|
||||
$raw = ($mode === 'raw');
|
||||
$livejsFile = $_REQUEST['item'] ?? '';
|
||||
|
||||
require '../app/classes/settings.php';
|
||||
$settingsObject = new Settings();
|
||||
|
||||
$livejsData = $settingsObject->getPlatformJsFile($platformDetails[0]['jitsi_url'], $item, $raw);
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/livejs.php';
|
|
@ -4,302 +4,105 @@
|
|||
* User login
|
||||
*
|
||||
* This page ("login") handles user login, session management, cookie handling, and error logging.
|
||||
* Supports "remember me" functionality to extend session duration and two-factor authentication.
|
||||
* Supports "remember me" functionality to extend session duration.
|
||||
*
|
||||
* Actions Performed:
|
||||
* - Validates login credentials
|
||||
* - Handles two-factor authentication if enabled
|
||||
* - Manages session and cookies based on "remember me" option
|
||||
* - Logs successful and failed login attempts
|
||||
* - Displays login form and optional custom messages
|
||||
* - Validates login credentials.
|
||||
* - Manages session and cookies based on "remember me" option.
|
||||
* - Logs successful and failed login attempts.
|
||||
* - Displays login form and optional custom messages.
|
||||
*/
|
||||
|
||||
// clear the global error var before login
|
||||
unset($error);
|
||||
|
||||
try {
|
||||
|
||||
// connect to database
|
||||
$db = connectDB($config);
|
||||
$dbWeb = connectDB($config);
|
||||
|
||||
// Initialize RateLimiter
|
||||
require_once '../app/classes/ratelimiter.php';
|
||||
$rateLimiter = new RateLimiter($db);
|
||||
// Get user IP
|
||||
require_once '../app/helpers/ip_helper.php';
|
||||
$user_IP = getUserIP();
|
||||
$rateLimiter = new RateLimiter($dbWeb['db']);
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
|
||||
if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) {
|
||||
// Handle 2FA verification
|
||||
$code = $_POST['code'] ?? '';
|
||||
$pending2FA = Session::get2FAPending();
|
||||
|
||||
if (!$pending2FA) {
|
||||
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once '../app/classes/twoFactorAuth.php';
|
||||
$twoFactorAuth = new TwoFactorAuthentication($db);
|
||||
|
||||
if ($twoFactorAuth->verify($pending2FA['user_id'], $code)) {
|
||||
// Complete login
|
||||
handleSuccessfulLogin($pending2FA['user_id'], $pending2FA['username'],
|
||||
$pending2FA['remember_me'], $config, $app_root, $logObject, $user_IP);
|
||||
|
||||
// Clean up 2FA session data
|
||||
Session::clear2FAPending();
|
||||
|
||||
exit();
|
||||
}
|
||||
|
||||
// If we get here (and we have code submitted), verification failed
|
||||
if (!empty($code)) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code');
|
||||
}
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Make userId available to template
|
||||
$userId = $pending2FA['user_id'];
|
||||
|
||||
// Load the 2FA verification template
|
||||
include '../app/templates/credentials-2fa-verify.php';
|
||||
exit();
|
||||
} elseif ($action === 'forgot') {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Handle password reset request
|
||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
||||
try {
|
||||
// Validate CSRF token
|
||||
$security = SecurityHelper::getInstance();
|
||||
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
throw new Exception('Invalid security token. Please try again.');
|
||||
}
|
||||
$username = $_POST['username'];
|
||||
$password = $_POST['password'];
|
||||
|
||||
// Apply rate limiting
|
||||
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
|
||||
if ($rateLimiter->isIpBlacklisted($user_IP)) {
|
||||
throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']);
|
||||
}
|
||||
if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) {
|
||||
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
|
||||
}
|
||||
$rateLimiter->attempt('password_reset', $user_IP);
|
||||
}
|
||||
|
||||
// Validate email
|
||||
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
|
||||
if (!$email) {
|
||||
throw new Exception('Please enter a valid email address.');
|
||||
}
|
||||
|
||||
// Process reset request
|
||||
require_once '../app/classes/passwordReset.php';
|
||||
$resetHandler = new PasswordReset($db, $config);
|
||||
$result = $resetHandler->requestReset($email);
|
||||
|
||||
// Always show same message whether email exists or not for security
|
||||
Feedback::flash('NOTICE', 'DEFAULT', $result['message']);
|
||||
header("Location: $app_root?page=login");
|
||||
exit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Generate CSRF token
|
||||
$security = SecurityHelper::getInstance();
|
||||
$security->generateCsrfToken();
|
||||
|
||||
// Load the forgot password form
|
||||
include '../app/helpers/feedback.php';
|
||||
include '../app/templates/form-password-forgot.php';
|
||||
exit();
|
||||
|
||||
} elseif ($action === 'reset' && isset($_GET['token'])) {
|
||||
// Handle password reset
|
||||
try {
|
||||
require_once '../app/classes/passwordReset.php';
|
||||
$resetHandler = new PasswordReset($db, $config);
|
||||
$token = $_GET['token'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Validate CSRF token
|
||||
$security = SecurityHelper::getInstance();
|
||||
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
throw new Exception('Invalid security token. Please try again.');
|
||||
}
|
||||
|
||||
// Apply rate limiting
|
||||
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
|
||||
if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) {
|
||||
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
|
||||
}
|
||||
$rateLimiter->attempt('password_reset', $user_IP);
|
||||
}
|
||||
|
||||
// Validate password
|
||||
require_once '../app/classes/validator.php';
|
||||
$validator = new Validator($_POST);
|
||||
$rules = [
|
||||
'new_password' => [
|
||||
'required' => true,
|
||||
'min' => 8
|
||||
],
|
||||
'confirm_password' => [
|
||||
'required' => true,
|
||||
'matches' => 'new_password'
|
||||
]
|
||||
];
|
||||
|
||||
if (!$validator->validate($rules)) {
|
||||
throw new Exception($validator->getFirstError());
|
||||
}
|
||||
|
||||
// Reset password
|
||||
if ($resetHandler->resetPassword($token, $_POST['new_password'])) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', 'Your password has been reset successfully. You can now log in.');
|
||||
header("Location: $app_root?page=login");
|
||||
exit();
|
||||
}
|
||||
throw new Exception('Invalid or expired reset link. Please request a new one.');
|
||||
}
|
||||
|
||||
// Verify token is valid
|
||||
$validation = $resetHandler->validateToken($token);
|
||||
if (!$validation['valid']) {
|
||||
throw new Exception('Invalid or expired reset link. Please request a new one.');
|
||||
}
|
||||
|
||||
// Show reset password form
|
||||
include '../app/helpers/feedback.php';
|
||||
include '../app/templates/form-password-reset.php';
|
||||
exit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
||||
header("Location: $app_root?page=login&action=forgot");
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' && $action !== 'verify' ) {
|
||||
try {
|
||||
// Validate form data
|
||||
$security = SecurityHelper::getInstance();
|
||||
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'remember_me', 'csrf_token']);
|
||||
|
||||
$validationRules = [
|
||||
'username' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'min' => 3,
|
||||
'max' => 20
|
||||
],
|
||||
'password' => [
|
||||
'type' => 'string',
|
||||
'required' => true
|
||||
]
|
||||
];
|
||||
|
||||
$errors = $security->validateFormData($formData, $validationRules);
|
||||
if (!empty($errors)) {
|
||||
throw new Exception("Invalid input: " . implode(", ", $errors));
|
||||
}
|
||||
|
||||
$username = $formData['username'];
|
||||
$password = $formData['password'];
|
||||
|
||||
// Skip all checks if IP is whitelisted
|
||||
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
|
||||
// Check if IP is blacklisted
|
||||
if ($rateLimiter->isIpBlacklisted($user_IP)) {
|
||||
throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']);
|
||||
throw new Exception(Messages::get('LOGIN', 'IP_BLACKLISTED')['message']);
|
||||
}
|
||||
|
||||
// Check rate limiting before recording attempt
|
||||
if ($rateLimiter->tooManyAttempts($username, $user_IP)) {
|
||||
throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']);
|
||||
// Check rate limiting (but skip if IP is whitelisted)
|
||||
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
|
||||
$attempts = $rateLimiter->getRecentAttempts($user_IP);
|
||||
if ($attempts >= $rateLimiter->maxAttempts) {
|
||||
throw new Exception(Messages::get('LOGIN', 'LOGIN_BLOCKED')['message']);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt login
|
||||
$loginResult = $userObject->login($username, $password);
|
||||
// login successful
|
||||
if ( $userObject->login($username, $password) ) {
|
||||
// if remember_me is checked, max out the session
|
||||
if (isset($_POST['remember_me'])) {
|
||||
// 30*24*60*60 = 30 days
|
||||
$cookie_lifetime = 30 * 24 * 60 * 60;
|
||||
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
|
||||
$gc_maxlifetime = 30 * 24 * 60 * 60;
|
||||
} else {
|
||||
// 0 - session end on browser close
|
||||
// 1440 - 24 minutes (default)
|
||||
$cookie_lifetime = 0;
|
||||
$setcookie_lifetime = 0;
|
||||
$gc_maxlifetime = 1440;
|
||||
}
|
||||
|
||||
if (is_array($loginResult)) {
|
||||
switch ($loginResult['status']) {
|
||||
case 'requires_2fa':
|
||||
// Store pending 2FA info
|
||||
Session::store2FAPending($loginResult['user_id'], $loginResult['username'],
|
||||
isset($formData['remember_me']));
|
||||
// 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'
|
||||
]);
|
||||
|
||||
// Redirect to 2FA verification
|
||||
header('Location: ?page=login&action=verify');
|
||||
// Log successful login
|
||||
$user_id = $userObject->getUserId($username)[0]['id'];
|
||||
$logObject->insertLog($user_id, "Login: User \"$username\" logged in. IP: $user_IP", 'user');
|
||||
|
||||
// Set success message and redirect
|
||||
Messages::flash('LOGIN', 'LOGIN_SUCCESS', null, true);
|
||||
header('Location: ' . htmlspecialchars($app_root));
|
||||
exit();
|
||||
|
||||
case 'success':
|
||||
// Complete login
|
||||
handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'],
|
||||
isset($formData['remember_me']), $config, $app_root, $logObject, $user_IP);
|
||||
exit();
|
||||
|
||||
default:
|
||||
throw new Exception($loginResult['message'] ?? 'Login failed');
|
||||
} else {
|
||||
throw new Exception(Messages::get('LOGIN', 'LOGIN_FAILED')['message']);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception(Feedback::get('LOGIN', 'LOGIN_FAILED')['message']);
|
||||
} catch (Exception $e) {
|
||||
// Log the failed attempt
|
||||
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
|
||||
Messages::flash('ERROR', 'DEFAULT', $e->getMessage());
|
||||
if (isset($username)) {
|
||||
$userId = $userObject->getUserId($username)[0]['id'] ?? 0;
|
||||
$logObject->log('error', "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", ['user_id' => $userId, 'scope' => 'user']);
|
||||
$rateLimiter->attempt($username, $user_IP);
|
||||
$user_id = $userObject->getUserId($username)[0]['id'] ?? 0;
|
||||
$logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Feedback::flash('ERROR', 'DEFAULT');
|
||||
Messages::flash('ERROR', 'DEFAULT', 'There was an unexpected error. Please try again.');
|
||||
}
|
||||
|
||||
// Show configured login message if any
|
||||
if (!empty($config['login_message'])) {
|
||||
echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false);
|
||||
echo Messages::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false);
|
||||
}
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/form-login.php';
|
||||
|
||||
/**
|
||||
* Handle successful login by setting up session and cookies
|
||||
*/
|
||||
function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $app_root, $logObject, $userIP) {
|
||||
// Create authenticated session
|
||||
Session::createAuthSession($userId, $username, $rememberMe, $config);
|
||||
|
||||
// Log successful login
|
||||
$logObject->log('info', "Login: User \"$username\" logged in. IP: $userIP", ['user_id' => $userId, 'scope' => 'user']);
|
||||
|
||||
// Set success message
|
||||
Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
|
||||
|
||||
// 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();
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Logs listings
|
||||
*
|
||||
* This page ("logs") retrieves and displays logs for a specified user within a time range.
|
||||
* It supports pagination and filtering, and generates a widget to display the logs.
|
||||
*/
|
||||
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
// Check for rights; user or system
|
||||
if (($userObject->hasRight($user_id, 'superuser') ||
|
||||
$userObject->hasRight($user_id, 'view app logs'))) {
|
||||
$scope = 'system';
|
||||
} else {
|
||||
$scope = 'user';
|
||||
}
|
||||
|
||||
// specify time range
|
||||
include '../app/helpers/time_range.php';
|
||||
|
||||
// pagination variables
|
||||
$items_per_page = 15;
|
||||
$browse_page = $_REQUEST['p'] ?? 1;
|
||||
$browse_page = (int)$browse_page;
|
||||
$offset = ($browse_page -1) * $items_per_page;
|
||||
|
||||
// prepare the result
|
||||
$search = $logObject->readLog($user_id, $scope, $offset, $items_per_page);
|
||||
$search_all = $logObject->readLog($user_id, $scope);
|
||||
|
||||
if (!empty($search)) {
|
||||
// we get total items and number of pages
|
||||
$item_count = count($search_all);
|
||||
$page_count = ceil($item_count / $items_per_page);
|
||||
|
||||
$logs = array();
|
||||
$logs['records'] = array();
|
||||
|
||||
foreach ($search as $item) {
|
||||
|
||||
// when we show only user's logs, omit user_id column
|
||||
if ($scope === 'user') {
|
||||
$log_record = array(
|
||||
// assign title to the field in the array record
|
||||
'time' => $item['time'],
|
||||
'log message' => $item['message']
|
||||
);
|
||||
} else {
|
||||
$log_record = array(
|
||||
// assign title to the field in the array record
|
||||
'userID' => $item['user_id'],
|
||||
'time' => $item['time'],
|
||||
'log message' => $item['message']
|
||||
);
|
||||
}
|
||||
|
||||
// populate the result array
|
||||
array_push($logs['records'], $log_record);
|
||||
}
|
||||
}
|
||||
|
||||
// prepare the widget
|
||||
$widget['full'] = false;
|
||||
$widget['collapsible'] = false;
|
||||
$widget['name'] = 'Logs';
|
||||
$username = $userObject->getUserDetails($user_id)[0]['username'];
|
||||
$widget['title'] = "Log events for user \"$username\"";
|
||||
$widget['filter'] = true;
|
||||
if (!empty($logs['records'])) {
|
||||
$widget['full'] = true;
|
||||
$widget['table_headers'] = array_keys($logs['records'][0]);
|
||||
$widget['table_records'] = $logs['records'];
|
||||
}
|
||||
$widget['pagination'] = true;
|
||||
|
||||
// display the widget
|
||||
include '../app/templates/logs-list.php';
|
||||
|
||||
?>
|
|
@ -8,12 +8,18 @@
|
|||
* Supports pagination.
|
||||
*/
|
||||
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
require '../app/classes/participant.php';
|
||||
|
||||
// connect to database
|
||||
$response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id);
|
||||
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
|
||||
|
||||
// if DB connection has error, display it and stop here
|
||||
if ($response['db'] === null) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
Messages::flash('ERROR', 'DEFAULT', $response['error']);
|
||||
|
||||
// otherwise if DB connection is OK, go on
|
||||
} else {
|
||||
|
@ -46,34 +52,13 @@ if ($response['db'] === null) {
|
|||
// Participant listings
|
||||
//
|
||||
|
||||
require '../app/classes/participant.php';
|
||||
$participantObject = new Participant($db);
|
||||
|
||||
// get current page for pagination
|
||||
$currentPage = $_REQUEST['page_num'] ?? 1;
|
||||
$currentPage = (int)$currentPage;
|
||||
|
||||
// pagination variables
|
||||
$items_per_page = 20;
|
||||
$offset = ($currentPage -1) * $items_per_page;
|
||||
|
||||
// Build params for pagination
|
||||
$params = '';
|
||||
if (!empty($_REQUEST['from_time'])) {
|
||||
$params .= '&from_time=' . urlencode($_REQUEST['from_time']);
|
||||
}
|
||||
if (!empty($_REQUEST['until_time'])) {
|
||||
$params .= '&until_time=' . urlencode($_REQUEST['until_time']);
|
||||
}
|
||||
if (!empty($_REQUEST['name'])) {
|
||||
$params .= '&name=' . urlencode($_REQUEST['name']);
|
||||
}
|
||||
if (!empty($_REQUEST['id'])) {
|
||||
$params .= '&id=' . urlencode($_REQUEST['id']);
|
||||
}
|
||||
if (isset($_REQUEST['event'])) {
|
||||
$params .= '&ip=' . urlencode($_REQUEST['ip']);
|
||||
}
|
||||
$items_per_page = 15;
|
||||
$browse_page = $_REQUEST['p'] ?? 1;
|
||||
$browse_page = (int)$browse_page;
|
||||
$offset = ($browse_page -1) * $items_per_page;
|
||||
|
||||
// search and list specific participant ID
|
||||
if (isset($participantId)) {
|
||||
|
@ -97,7 +82,7 @@ if ($response['db'] === null) {
|
|||
if (!empty($search)) {
|
||||
// we get total items and number of pages
|
||||
$item_count = count($search_all);
|
||||
$totalPages = ceil($item_count / $items_per_page);
|
||||
$page_count = ceil($item_count / $items_per_page);
|
||||
|
||||
$participants = array();
|
||||
$participants['records'] = array();
|
||||
|
@ -159,20 +144,34 @@ if ($response['db'] === null) {
|
|||
}
|
||||
}
|
||||
|
||||
// filter message
|
||||
$filterMessage = array();
|
||||
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
|
||||
array_push($filterMessage, 'Conferences with participant name (stats_id) matching "<strong>' . $_REQUEST['name'] . '</strong>"');
|
||||
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
|
||||
array_push($filterMessage, 'Conferences with participant ID matching "<strong>' . $_REQUEST['id'] . '</strong>"');
|
||||
} elseif (isset($participantIp)) {
|
||||
array_push($filterMessage, 'Conferences with participant IP matching "<strong>' . $participantIp . '</strong>"');
|
||||
}
|
||||
// prepare the widget
|
||||
$widget['full'] = false;
|
||||
$widget['name'] = 'Participants';
|
||||
$widget['collapsible'] = false;
|
||||
$widget['collapsed'] = false;
|
||||
$widget['filter'] = true;
|
||||
$widget['pagination'] = true;
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// widget title
|
||||
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
|
||||
$widget['title'] = 'Conferences with participant name (stats_id) matching "<strong>' . $_REQUEST['name'] . '"</strong>';
|
||||
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
|
||||
$widget['title'] = 'Conference with participant ID matching "<strong>' . $_REQUEST['id'] . '"</strong>';
|
||||
} elseif (isset($participantIp)) {
|
||||
$widget['title'] = 'Conference with participant IP matching "<strong>' . $participantIp . '"</strong>';
|
||||
} else {
|
||||
$widget['title'] = 'All participants';
|
||||
}
|
||||
// widget records
|
||||
if (!empty($participants['records'])) {
|
||||
$widget['full'] = true;
|
||||
$widget['table_headers'] = array_keys($participants['records'][0]);
|
||||
$widget['table_records'] = $participants['records'];
|
||||
}
|
||||
|
||||
// display the widget
|
||||
include '../app/templates/participants.php';
|
||||
include '../app/templates/widget.php';
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -12,47 +12,24 @@
|
|||
* - `edit`: Edit user profile details, rights, or avatar.
|
||||
*/
|
||||
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$item = $_REQUEST['item'] ?? '';
|
||||
|
||||
// if a form is submitted, it's from the edit page
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
// Validate CSRF token
|
||||
require_once '../app/helpers/security.php';
|
||||
$security = SecurityHelper::getInstance();
|
||||
if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid security token. Please try again.');
|
||||
header("Location: $app_root?page=profile");
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once '../app/classes/validator.php';
|
||||
|
||||
// Apply rate limiting for profile operations
|
||||
require_once '../app/includes/rate_limit_middleware.php';
|
||||
checkRateLimit($db, 'profile', $userId);
|
||||
$item = $_REQUEST['item'] ?? '';
|
||||
|
||||
// avatar removal
|
||||
if ($item === 'avatar' && $action === 'remove') {
|
||||
$validator = new Validator(['user_id' => $userId]);
|
||||
$rules = [
|
||||
'user_id' => [
|
||||
'required' => true,
|
||||
'numeric' => true
|
||||
]
|
||||
];
|
||||
|
||||
if (!$validator->validate($rules)) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
|
||||
header("Location: $app_root?page=profile");
|
||||
exit();
|
||||
}
|
||||
|
||||
$result = $userObject->removeAvatar($userId, $config['avatars_path'].$userDetails[0]['avatar']);
|
||||
$result = $userObject->removeAvatar($user_id, $config['avatars_path'].$userDetails[0]['avatar']);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "Avatar for user \"{$userDetails[0]['username']}\" is removed.");
|
||||
$_SESSION['notice'] .= "Avatar for user \"{$userDetails[0]['username']}\" is removed. ";
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Removing the avatar failed. Error: $result");
|
||||
$_SESSION['error'] .= "Removing the avatar failed. Error: $result ";
|
||||
}
|
||||
|
||||
header("Location: $app_root?page=profile");
|
||||
|
@ -60,83 +37,41 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
|||
}
|
||||
|
||||
// update the profile
|
||||
$validator = new Validator($_POST);
|
||||
$rules = [
|
||||
'name' => [
|
||||
'max' => 100
|
||||
],
|
||||
'email' => [
|
||||
'email' => true,
|
||||
'max' => 100
|
||||
],
|
||||
'timezone' => [
|
||||
'max' => 50
|
||||
],
|
||||
'bio' => [
|
||||
'max' => 1000
|
||||
]
|
||||
];
|
||||
|
||||
if (!$validator->validate($rules)) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
|
||||
header("Location: $app_root?page=profile");
|
||||
exit();
|
||||
}
|
||||
|
||||
$updatedUser = [
|
||||
'name' => htmlspecialchars($_POST['name'] ?? ''),
|
||||
'email' => filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL),
|
||||
'timezone' => htmlspecialchars($_POST['timezone'] ?? ''),
|
||||
'bio' => htmlspecialchars($_POST['bio'] ?? ''),
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'email' => $_POST['email'] ?? '',
|
||||
'timezone' => $_POST['timezone'] ?? '',
|
||||
'bio' => $_POST['bio'] ?? '',
|
||||
];
|
||||
$result = $userObject->editUser($userId, $updatedUser);
|
||||
$result = $userObject->editUser($user_id, $updatedUser);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "User details for \"{$userDetails[0]['username']}\" are edited.");
|
||||
$_SESSION['notice'] .= "User details for \"{$updatedUser['name']}\" are edited. ";
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Editing the user details failed. Error: $result");
|
||||
$_SESSION['error'] .= "Editing the user details failed. Error: $result ";
|
||||
}
|
||||
|
||||
// update the rights
|
||||
// Get current rights IDs
|
||||
$newRights = $_POST['rights'] ?? array();
|
||||
// extract the new right_ids
|
||||
$userRightsIds = array_column($userRights, 'right_id');
|
||||
|
||||
// If no rights are selected, remove all rights
|
||||
if (!isset($_POST['rights'])) {
|
||||
$_POST['rights'] = [];
|
||||
}
|
||||
|
||||
$validator = new Validator(['rights' => $_POST['rights']]);
|
||||
$rules = [
|
||||
'rights' => [
|
||||
'array' => true
|
||||
]
|
||||
];
|
||||
|
||||
if (!$validator->validate($rules)) {
|
||||
Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError());
|
||||
header("Location: $app_root?page=profile");
|
||||
exit();
|
||||
}
|
||||
|
||||
$newRights = $_POST['rights'];
|
||||
// what rights we need to add
|
||||
$rightsToAdd = array_diff($newRights, $userRightsIds);
|
||||
if (!empty($rightsToAdd)) {
|
||||
foreach ($rightsToAdd as $rightId) {
|
||||
$userObject->addUserRight($userId, $rightId);
|
||||
$userObject->addUserRight($user_id, $rightId);
|
||||
}
|
||||
}
|
||||
// what rights we need to remove
|
||||
$rightsToRemove = array_diff($userRightsIds, $newRights);
|
||||
if (!empty($rightsToRemove)) {
|
||||
foreach ($rightsToRemove as $rightId) {
|
||||
$userObject->removeUserRight($userId, $rightId);
|
||||
$userObject->removeUserRight($user_id, $rightId);
|
||||
}
|
||||
}
|
||||
|
||||
// update the avatar
|
||||
if (!empty($_FILES['avatar_file']['tmp_name'])) {
|
||||
$result = $userObject->changeAvatar($userId, $_FILES['avatar_file'], $config['avatars_path']);
|
||||
$result = $userObject->changeAvatar($user_id, $_FILES['avatar_file'], $config['avatars_path']);
|
||||
}
|
||||
|
||||
header("Location: $app_root?page=profile");
|
||||
|
@ -147,30 +82,19 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
|||
$avatar = !empty($userDetails[0]['avatar']) ? $config['avatars_path'] . $userDetails[0]['avatar'] : $config['default_avatar'];
|
||||
$default_avatar = empty($userDetails[0]['avatar']) ? true : false;
|
||||
|
||||
// Generate CSRF token if not exists
|
||||
require_once '../app/helpers/security.php';
|
||||
$security = SecurityHelper::getInstance();
|
||||
$security->generateCsrfToken();
|
||||
|
||||
switch ($action) {
|
||||
|
||||
case 'edit':
|
||||
$allRights = $userObject->getAllRights();
|
||||
$allTimezones = timezone_identifiers_list();
|
||||
// if timezone is already set, we pass a flag for JS to not autodetect browser timezone
|
||||
$isTimezoneSet = !empty($userDetails[0]['timezone']);
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/profile-edit.php';
|
||||
break;
|
||||
|
||||
default:
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/profile.php';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* User registration
|
||||
*
|
||||
* This page ("register") handles user registration if the feature is enabled in the configuration.
|
||||
* It accepts a POST request with a username and password, attempts to register the user,
|
||||
* and redirects to the login page on success or displays an error message on failure.
|
||||
*/
|
||||
|
||||
// registration is allowed, go on
|
||||
if ($config['registration_enabled'] === true) {
|
||||
|
||||
try {
|
||||
|
||||
// connect to database
|
||||
$dbWeb = connectDB($config);
|
||||
|
||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
||||
$username = $_POST['username'];
|
||||
$password = $_POST['password'];
|
||||
|
||||
// registering
|
||||
$result = $userObject->register($username, $password);
|
||||
|
||||
// redirect to login
|
||||
if ($result === true) {
|
||||
Messages::flash('NOTICE', 'DEFAULT', "Registration successful.<br />You can log in now.");
|
||||
header('Location: ' . htmlspecialchars($app_root));
|
||||
exit();
|
||||
// registration fail, redirect to login
|
||||
} else {
|
||||
Messages::flash('ERROR', 'DEFAULT', "Registration failed. $result");
|
||||
header('Location: ' . htmlspecialchars($app_root));
|
||||
exit();
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Messages::flash('ERROR', 'DEFAULT', $e->getMessage());
|
||||
}
|
||||
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/form-register.php';
|
||||
|
||||
// registration disabled
|
||||
} else {
|
||||
echo Messages::render('NOTICE', 'DEFAULT', 'Registration is disabled', false);
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,10 +1,15 @@
|
|||
<?php
|
||||
|
||||
// Check if user has any of the required rights
|
||||
if (!($userObject->hasRight($userId, 'superuser') ||
|
||||
$userObject->hasRight($userId, 'edit whitelist') ||
|
||||
$userObject->hasRight($userId, 'edit blacklist') ||
|
||||
$userObject->hasRight($userId, 'edit ratelimiting'))) {
|
||||
if (!($userObject->hasRight($user_id, 'superuser') ||
|
||||
$userObject->hasRight($user_id, 'edit whitelist') ||
|
||||
$userObject->hasRight($user_id, 'edit blacklist') ||
|
||||
$userObject->hasRight($user_id, 'edit ratelimiting'))) {
|
||||
include '../app/templates/error-unauthorized.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($currentUser)) {
|
||||
include '../app/templates/error-unauthorized.php';
|
||||
exit;
|
||||
}
|
||||
|
@ -14,161 +19,95 @@ $section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section'
|
|||
|
||||
// Initialize RateLimiter
|
||||
require_once '../app/classes/ratelimiter.php';
|
||||
$rateLimiter = new RateLimiter($db);
|
||||
$rateLimiter = new RateLimiter($dbWeb);
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
require_once '../app/classes/validator.php';
|
||||
|
||||
// Apply rate limiting for security operations
|
||||
require_once '../app/includes/rate_limit_middleware.php';
|
||||
checkRateLimit($db, 'security', $userId);
|
||||
|
||||
$action = $_POST['action'];
|
||||
$validator = new Validator($_POST);
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'add_whitelist':
|
||||
if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) {
|
||||
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
||||
break;
|
||||
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) {
|
||||
throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'ip_address' => [
|
||||
'required' => true,
|
||||
'max' => 45, // Max length for IPv6
|
||||
'ip' => true
|
||||
],
|
||||
'description' => [
|
||||
'required' => true,
|
||||
'max' => 255
|
||||
]
|
||||
];
|
||||
|
||||
if ($validator->validate($rules)) {
|
||||
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
|
||||
if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $userId)) {
|
||||
Feedback::flash('SECURITY', 'WHITELIST_ADD_FAILED');
|
||||
} else {
|
||||
Feedback::flash('SECURITY', 'WHITELIST_ADD_SUCCESS');
|
||||
if (empty($_POST['ip_address'])) {
|
||||
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
|
||||
}
|
||||
} else {
|
||||
Feedback::flash('SECURITY', 'WHITELIST_ADD_ERROR_IP', $validator->getFirstError());
|
||||
$is_network = isset($_POST['is_network']) ? 1 : 0;
|
||||
if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $user_id)) {
|
||||
throw new Exception(Messages::get('SECURITY', 'WHITELIST_ADD_ERROR')['message']);
|
||||
}
|
||||
Messages::flash('SECURITY', 'WHITELIST_ADD_SUCCESS');
|
||||
break;
|
||||
|
||||
case 'remove_whitelist':
|
||||
if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) {
|
||||
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
||||
break;
|
||||
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) {
|
||||
throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'ip_address' => [
|
||||
'required' => true,
|
||||
'max' => 45,
|
||||
'ip' => true
|
||||
]
|
||||
];
|
||||
|
||||
if ($validator->validate($rules)) {
|
||||
if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $userId)) {
|
||||
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED');
|
||||
} else {
|
||||
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS');
|
||||
if (empty($_POST['ip_address'])) {
|
||||
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
|
||||
}
|
||||
} else {
|
||||
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED', $validator->getFirstError());
|
||||
if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $user_id)) {
|
||||
throw new Exception(Messages::get('SECURITY', 'WHITELIST_REMOVE_ERROR')['message']);
|
||||
}
|
||||
Messages::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS');
|
||||
break;
|
||||
|
||||
case 'add_blacklist':
|
||||
if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) {
|
||||
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
||||
break;
|
||||
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) {
|
||||
throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'ip_address' => [
|
||||
'required' => true,
|
||||
'max' => 45,
|
||||
'ip' => true
|
||||
],
|
||||
'reason' => [
|
||||
'required' => true,
|
||||
'max' => 255
|
||||
],
|
||||
'expiry_hours' => [
|
||||
'numeric' => true,
|
||||
'min' => 0,
|
||||
'max' => 8760 // 1 year in hours
|
||||
]
|
||||
];
|
||||
|
||||
if ($validator->validate($rules)) {
|
||||
$is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on';
|
||||
$expiry_hours = !empty($_POST['expiry_hours']) ? (int)$_POST['expiry_hours'] : null;
|
||||
|
||||
if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'], $currentUser, $userId, $expiry_hours)) {
|
||||
Feedback::flash('SECURITY', 'BLACKLIST_ADD_FAILED');
|
||||
} else {
|
||||
Feedback::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS');
|
||||
if (empty($_POST['ip_address'])) {
|
||||
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
|
||||
}
|
||||
} else {
|
||||
Feedback::flash('SECURITY', 'BLACKLIST_ADD_ERROR_IP', $validator->getFirstError());
|
||||
$is_network = isset($_POST['is_network']) ? 1 : 0;
|
||||
$expiry_hours = !empty($_POST['expiry_hours']) ? intval($_POST['expiry_hours']) : null;
|
||||
if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'] ?? '', $currentUser, $user_id, $expiry_hours)) {
|
||||
throw new Exception(Messages::get('SECURITY', 'BLACKLIST_ADD_ERROR')['message']);
|
||||
}
|
||||
Messages::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS');
|
||||
break;
|
||||
|
||||
case 'remove_blacklist':
|
||||
if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) {
|
||||
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
||||
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) {
|
||||
throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']);
|
||||
}
|
||||
if (empty($_POST['ip_address'])) {
|
||||
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
|
||||
}
|
||||
if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $user_id)) {
|
||||
throw new Exception(Messages::get('SECURITY', 'BLACKLIST_REMOVE_ERROR')['message']);
|
||||
}
|
||||
Messages::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS');
|
||||
break;
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'ip_address' => [
|
||||
'required' => true,
|
||||
'max' => 45,
|
||||
'ip' => true
|
||||
]
|
||||
];
|
||||
|
||||
if ($validator->validate($rules)) {
|
||||
if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $userId)) {
|
||||
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED');
|
||||
} else {
|
||||
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS');
|
||||
}
|
||||
} else {
|
||||
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED', $validator->getFirstError());
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Feedback::flash('ERROR', 'INVALID_ACTION');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Feedback::flash('ERROR', $e->getMessage());
|
||||
$messages[] = ['category' => 'SECURITY', 'key' => 'CUSTOM_ERROR', 'custom_message' => $e->getMessage()];
|
||||
Messages::flash('SECURITY', 'CUSTOM_ERROR', 'custom_message');
|
||||
}
|
||||
|
||||
// Redirect back to the appropriate section
|
||||
header("Location: $app_root?page=security§ion=" . urlencode($section));
|
||||
if (empty($messages)) {
|
||||
// Only redirect if there were no errors
|
||||
header("Location: {$app_root}?page=security§ion={$section}");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Always show rate limit info message for rate limiting section
|
||||
if ($section === 'ratelimit') {
|
||||
$system_messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO'];
|
||||
$messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO'];
|
||||
}
|
||||
|
||||
// Get current lists
|
||||
$whitelisted = $rateLimiter->getWhitelistedIps();
|
||||
$blacklisted = $rateLimiter->getBlacklistedIps();
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/security.php';
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,179 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Jilo settings management.
|
||||
*
|
||||
* This page ("settings") handles Jilo settings by
|
||||
* adding, editing, and deleting platforms, hosts, agents.
|
||||
*/
|
||||
|
||||
// Check if this is an AJAX request
|
||||
$isAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$agent = $_REQUEST['agent'] ?? '';
|
||||
$host = $_REQUEST['host'] ?? '';
|
||||
|
||||
require '../app/classes/host.php';
|
||||
require '../app/classes/agent.php';
|
||||
|
||||
$hostObject = new Host($db);
|
||||
$agentObject = new Agent($db);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
/**
|
||||
* Handles form submissions from editing
|
||||
*/
|
||||
|
||||
// Apply rate limiting for profile operations
|
||||
require_once '../app/includes/rate_limit_middleware.php';
|
||||
checkRateLimit($db, 'profile', $userId);
|
||||
|
||||
// Get hash from URL if present
|
||||
$hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? '';
|
||||
$redirectUrl = htmlspecialchars($app_root) . '?page=settings';
|
||||
if ($hash) {
|
||||
$redirectUrl .= '#' . $hash;
|
||||
}
|
||||
|
||||
// host operations
|
||||
if (isset($_POST['item']) && $_POST['item'] === 'host') {
|
||||
if (isset($_POST['delete']) && $_POST['delete'] === 'true') { // This is a host delete
|
||||
$host_id = $_POST['host'];
|
||||
$result = $hostObject->deleteHost($host_id);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "Host deleted successfully.", true);
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Deleting the host failed. Error: $result", true);
|
||||
}
|
||||
} else if (!isset($_POST['host'])) { // This is a new host
|
||||
$newHost = [
|
||||
'address' => $_POST['address'],
|
||||
'platform_id' => $_POST['platform'],
|
||||
'name' => empty($_POST['name']) ? $_POST['address'] : $_POST['name'],
|
||||
];
|
||||
$result = $hostObject->addHost($newHost);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "New Jilo host added.", true);
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Adding the host failed. Error: $result", true);
|
||||
}
|
||||
} else { // This is an edit of existing host
|
||||
$host_id = $_POST['host'];
|
||||
$platform_id = $_POST['platform'];
|
||||
$updatedHost = [
|
||||
'id' => $host_id,
|
||||
'address' => $_POST['address'],
|
||||
'name' => empty($_POST['name']) ? $_POST['address'] : $_POST['name'],
|
||||
];
|
||||
$result = $hostObject->editHost($platform_id, $updatedHost);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "Host edited.", true);
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Editing the host failed. Error: $result", true);
|
||||
}
|
||||
}
|
||||
if (!$isAjax) {
|
||||
header('Location: ' . $redirectUrl);
|
||||
exit;
|
||||
}
|
||||
|
||||
// agent operations
|
||||
} elseif (isset($_POST['item']) && $_POST['item'] === 'agent') {
|
||||
if (isset($_POST['delete']) && $_POST['delete'] === 'true') { // This is an agent delete
|
||||
$agent_id = $_POST['agent'];
|
||||
$result = $agentObject->deleteAgent($agent_id);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "Agent deleted successfully.", true);
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Deleting the agent failed. Error: $result", true);
|
||||
}
|
||||
} else if (isset($_POST['new']) && $_POST['new'] === 'true') { // This is a new agent
|
||||
$newAgent = [
|
||||
'type_id' => $_POST['type'],
|
||||
'url' => $_POST['url'],
|
||||
'secret_key' => empty($_POST['secret_key']) ? null : $_POST['secret_key'],
|
||||
'check_period' => empty($_POST['check_period']) ? 0 : $_POST['check_period'],
|
||||
];
|
||||
$result = $agentObject->addAgent($_POST['host'], $newAgent);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "New Jilo agent added.", true);
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Adding the agent failed. Error: $result", true);
|
||||
}
|
||||
} else { // This is an edit of existing agent
|
||||
$agent_id = $_POST['agent'];
|
||||
$updatedAgent = [
|
||||
'agent_type_id' => $_POST['agent_type_id'],
|
||||
'url' => $_POST['url'],
|
||||
'secret_key' => empty($_POST['secret_key']) ? null : $_POST['secret_key'],
|
||||
'check_period' => empty($_POST['check_period']) ? 0 : $_POST['check_period'],
|
||||
];
|
||||
$result = $agentObject->editAgent($agent_id, $updatedAgent);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "Agent edited.", true);
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Editing the agent failed. Error: $result", true);
|
||||
}
|
||||
}
|
||||
if (!$isAjax) {
|
||||
header('Location: ' . $redirectUrl);
|
||||
exit;
|
||||
}
|
||||
|
||||
// platform operations
|
||||
} elseif (isset($_POST['item']) && $_POST['item'] === 'platform') {
|
||||
if (isset($_POST['delete']) && $_POST['delete'] === 'true') { // This is a platform delete
|
||||
$platform_id = $_POST['platform'];
|
||||
$result = $platformObject->deletePlatform($platform_id);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "Platform deleted successfully.", true);
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Deleting the platform failed. Error: $result", true);
|
||||
}
|
||||
} else if (!isset($_POST['platform'])) { // This is a new platform
|
||||
$newPlatform = [
|
||||
'name' => $_POST['name'],
|
||||
'jitsi_url' => $_POST['jitsi_url'],
|
||||
'jilo_database' => $_POST['jilo_database'],
|
||||
];
|
||||
$result = $platformObject->addPlatform($newPlatform);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "New Jitsi platform added.", true);
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Adding the platform failed. Error: $result", true);
|
||||
}
|
||||
} else { // This is an edit of existing platform
|
||||
$platform_id = $_POST['platform'];
|
||||
$updatedPlatform = [
|
||||
'name' => $_POST['name'],
|
||||
'jitsi_url' => $_POST['jitsi_url'],
|
||||
'jilo_database' => $_POST['jilo_database'],
|
||||
];
|
||||
$result = $platformObject->editPlatform($platform_id, $updatedPlatform);
|
||||
if ($result === true) {
|
||||
Feedback::flash('NOTICE', 'DEFAULT', "Platform edited.", true);
|
||||
} else {
|
||||
Feedback::flash('ERROR', 'DEFAULT', "Editing the platform failed. Error: $result", true);
|
||||
}
|
||||
}
|
||||
header('Location: ' . $redirectUrl);
|
||||
exit;
|
||||
}
|
||||
|
||||
} else {
|
||||
/**
|
||||
* Handles GET requests to display templates.
|
||||
*/
|
||||
|
||||
if ($userObject->hasRight($userId, 'view settings') || $userObject->hasRight($userId, 'superuser')) {
|
||||
$jilo_agent_types = $agentObject->getAgentTypes();
|
||||
include '../app/templates/settings.php';
|
||||
} else {
|
||||
include '../app/templates/error-unauthorized.php';
|
||||
}
|
||||
}
|
|
@ -8,13 +8,12 @@
|
|||
* It generates output for each platform and agent.
|
||||
*/
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
// Get any new messages
|
||||
include '../app/includes/messages.php';
|
||||
include '../app/includes/messages-show.php';
|
||||
|
||||
require '../app/classes/agent.php';
|
||||
require '../app/classes/host.php';
|
||||
$agentObject = new Agent($db);
|
||||
$hostObject = new Host($db);
|
||||
$agentObject = new Agent($dbWeb);
|
||||
|
||||
include '../app/templates/status-server.php';
|
||||
|
||||
|
@ -22,26 +21,21 @@ include '../app/templates/status-server.php';
|
|||
foreach ($platformsAll as $platform) {
|
||||
|
||||
// check if we can connect to the jilo database
|
||||
$response = connectJiloDB($config, $platform['jilo_database'], $platform['id']);
|
||||
$response = connectDB($config, 'jilo', $platform['jilo_database'], $platform['id']);
|
||||
if ($response['error'] !== null) {
|
||||
$jilo_database_status = $response['error'];
|
||||
$jilo_database_status = '<span class="text-danger">' . htmlspecialchars($response['error']) . '</span>';
|
||||
} else {
|
||||
$jilo_database_status = 'Connected';
|
||||
$jilo_database_status = '<span class="text-success">OK</span>';
|
||||
}
|
||||
|
||||
include '../app/templates/status-platform.php';
|
||||
|
||||
// fetch hosts for the current platform
|
||||
$hostDetails = $hostObject->getHostDetails($platform['id']);
|
||||
foreach ($hostDetails as $host) {
|
||||
// fetch agent details for the current host
|
||||
$agentDetails = $agentObject->getAgentDetails($host['id']);
|
||||
// fetch agent details for the current platform
|
||||
$agentDetails = $agentObject->getAgentDetails($platform['id']);
|
||||
foreach ($agentDetails as $agent) {
|
||||
// we try to parse the URL to scheme:/host:port
|
||||
$agent_url = parse_url($agent['url']);
|
||||
$agent_protocol = isset($agent_url['scheme']) ? $agent_url['scheme']: '';
|
||||
// on failure we keep the full value for displaying purpose
|
||||
$agent_host = isset($agent_url['host']) ? $agent_url['host']: $agent['url'];
|
||||
$agent_host = isset($agent_url['host']) ? $agent_url['host']: '';
|
||||
$agent_port = isset($agent_url['port']) ? $agent_url['port']: '';
|
||||
|
||||
// we get agent data to check availability
|
||||
|
@ -50,19 +44,19 @@ foreach ($platformsAll as $platform) {
|
|||
|
||||
// determine agent availability based on response data
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$agent_availability = 'unknown';
|
||||
$agent_availability = '<span class="text-warning">unknown</span>';
|
||||
foreach ($agent_data as $key => $value) {
|
||||
if ($key === 'error') {
|
||||
$agent_availability = $value;
|
||||
$agent_availability = '<span class="text-danger">' . htmlspecialchars($value) . '</span>';
|
||||
break;
|
||||
}
|
||||
if (preg_match('/_state$/', $key)) {
|
||||
if ($value === 'error') {
|
||||
$agent_availability = 'not running';
|
||||
$agent_availability = '<span class="text-danger">not running</span>';
|
||||
break;
|
||||
}
|
||||
if ($value === 'running') {
|
||||
$agent_availability = 'running';
|
||||
$agent_availability = '<span class="text-success">running</span>';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -74,8 +68,5 @@ foreach ($platformsAll as $platform) {
|
|||
include '../app/templates/status-agent.php';
|
||||
}
|
||||
}
|
||||
echo "\n\t\t\t\t\t\t\t</div>\n";
|
||||
}
|
||||
echo "\n\t\t\t\t\t\t</div>";
|
||||
echo "\n\t\t\t\t\t</div>";
|
||||
echo "\n\t\t\t\t</div>";
|
||||
|
||||
?>
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Theme Asset handler
|
||||
*
|
||||
* Serves theme assets through the main application router.
|
||||
* This provides a secure way to serve theme files that are outside the web root.
|
||||
*/
|
||||
|
||||
// Include the theme asset handler
|
||||
require_once __DIR__ . '/../helpers/theme-asset.php';
|
|
@ -1,71 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Theme Management Controller
|
||||
*
|
||||
* Handles theme switching and management functionality.
|
||||
* Allows users to view available themes and change the active theme.
|
||||
*
|
||||
* Actions:
|
||||
* - switch_to: Changes the active theme for the current user
|
||||
*/
|
||||
|
||||
// Initialize security
|
||||
require_once '../app/helpers/security.php';
|
||||
$security = SecurityHelper::getInstance();
|
||||
|
||||
// Only allow access to logged-in users
|
||||
if (!Session::isValidSession()) {
|
||||
header('Location: ' . $app_root . '?page=login');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle theme switching
|
||||
if (isset($_GET['switch_to'])) {
|
||||
$themeName = $_GET['switch_to'];
|
||||
|
||||
// Validate CSRF token for state-changing operations
|
||||
if (!$security->verifyCsrfToken($_GET['csrf_token'] ?? '')) {
|
||||
Feedback::flash('SECURITY', 'CSRF_INVALID');
|
||||
header("Location: $app_root?page=theme");
|
||||
exit();
|
||||
}
|
||||
|
||||
if (\App\Helpers\Theme::setCurrentTheme($themeName)) {
|
||||
// Set success message
|
||||
Feedback::flash('THEME', 'THEME_CHANGED');
|
||||
} else {
|
||||
// Set error message
|
||||
Feedback::flash('THEME', 'THEME_CHANGE_FAILED');
|
||||
}
|
||||
|
||||
// Redirect back to prevent form resubmission
|
||||
$redirect = $app_root . '?page=theme';
|
||||
header("Location: $redirect");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get available themes and current theme for the view
|
||||
$themes = \App\Helpers\Theme::getAvailableThemes();
|
||||
$currentTheme = \App\Helpers\Theme::getCurrentThemeName();
|
||||
|
||||
// Prepare theme data with screenshot URLs for the view
|
||||
$themeData = [];
|
||||
foreach ($themes as $id => $name) {
|
||||
$themeData[$id] = [
|
||||
'name' => $name,
|
||||
'screenshotUrl' => \App\Helpers\Theme::getAssetUrl($id, 'screenshot.png'),
|
||||
'isActive' => $id === $currentTheme
|
||||
];
|
||||
}
|
||||
|
||||
// Make theme data available to the view
|
||||
$themes = $themeData;
|
||||
|
||||
// Generate CSRF token for the form
|
||||
$csrf_token = $security->generateCsrfToken();
|
||||
|
||||
// Get any new feedback messages
|
||||
include '../app/helpers/feedback.php';
|
||||
|
||||
// Load the template
|
||||
include '../app/templates/theme.php';
|
|
@ -0,0 +1,39 @@
|
|||
|
||||
<!-- jilo agents -->
|
||||
<div class="card text-center w-75 mx-lef">
|
||||
<p class="h4 card-header">Jilo Agents on platform <?= htmlspecialchars($platform_id) ?> (<?= htmlspecialchars($platformDetails[0]['name']) ?>)</p>
|
||||
<div class="card-body">
|
||||
<?php foreach ($agentDetails as $agent) { ?>
|
||||
<p class="card-text text-left" style="text-align: left;">
|
||||
agent id: <strong><?= htmlspecialchars($agent['id']) ?></strong>
|
||||
agent type: <?= htmlspecialchars($agent['agent_type_id']) ?> (<strong><?= htmlspecialchars($agent['agent_description']) ?></strong>)
|
||||
<br />
|
||||
endpoint: <strong><?= htmlspecialchars($agent['url']) ?><?= htmlspecialchars($agent['agent_endpoint']) ?></strong>
|
||||
<br />
|
||||
<?php
|
||||
$payload = [
|
||||
'iss' => 'Jilo Web',
|
||||
'aud' => $config['domain'],
|
||||
'iat' => time(),
|
||||
'exp' => time() + 3600,
|
||||
'agent_id' => $agent['id']
|
||||
];
|
||||
$jwt = $agentObject->generateAgentToken($payload, $agent['secret_key']);
|
||||
// print_r($_SESSION);
|
||||
?>
|
||||
<?php if (isset($_SESSION["agent{$agent['id']}_cache"])) { ?>
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-status" class="btn btn-primary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="get the agent status" onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '/status', '<?= htmlspecialchars($jwt) ?>', true)">get status</button>
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-fetch" class="btn btn-primary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="get data from the agent" onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '<?= htmlspecialchars($agent['agent_endpoint']) ?>', '<?= htmlspecialchars($jwt) ?>', true)">fetch data</button>
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-cache" class="btn btn-secondary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="load cache" onclick="loadCache('<?= htmlspecialchars($agent['id']) ?>')">load cache</button>
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-clear" class="btn btn-danger" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="clear cache" onclick="clearCache('<?= htmlspecialchars($agent['id']) ?>')">clear cache</button>
|
||||
<span id="cacheInfo<?= htmlspecialchars($agent['id']) ?>" style="margin: 5px 0;"></span>
|
||||
<?php } else { ?>
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-status" class="btn btn-primary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="get the agent status" onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '/status', '<?= htmlspecialchars($jwt) ?>', true)">get status</button>
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-fetch" class="btn btn-primary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="get data from the agent" onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '<?= htmlspecialchars($agent['agent_endpoint']) ?>', '<?= htmlspecialchars($jwt) ?>')">fetch data</button>
|
||||
<button style="display: none" disabled id="agent<?= htmlspecialchars($agent['id']) ?>-cache" class="btn btn-secondary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="load cache" onclick="loadCache('<?= htmlspecialchars($agent['id']) ?>')">load cache</button>
|
||||
<button style="display: none" disabled id="agent<?= htmlspecialchars($agent['id']) ?>-clear" class="btn btn-danger" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="clear cache" onclick="clearCache('<?= htmlspecialchars($agent['id']) ?>')">clear cache</button>
|
||||
<span style="display: none" id="cacheInfo<?= htmlspecialchars($agent['id']) ?>" style="margin: 5px 0;"></span>
|
||||
<?php } ?>
|
||||
</p>
|
||||
<pre class="results" id="result<?= htmlspecialchars($agent['id']) ?>">click a button to display data from the agent.</pre>
|
||||
<?php } ?>
|
|
@ -1,94 +0,0 @@
|
|||
|
||||
<!-- agents live data -->
|
||||
<div class="container-fluid mt-2">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12 mb-4">
|
||||
<h2 class="mb-0">Jilo Agents status</h2>
|
||||
<small>manage and monitor agents on platform <strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- hosts and their agents -->
|
||||
<div class="row">
|
||||
<?php foreach ($agentsByHost as $hostId => $hostData): ?>
|
||||
<div class="col-12 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-network-wired me-2 text-secondary"></i>
|
||||
Host: <?= htmlspecialchars($hostData['host_name']) ?>
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?page=settings#platform-<?= htmlspecialchars($platform_id) ?>host-<?= htmlspecialchars($hostId) ?>" class="text-decoration-none">
|
||||
<i class="fas fa-edit ms-2"></i>
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (empty($hostData['agents'])): ?>
|
||||
<p class="text-muted">No agents on this host.</p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($hostData['agents'] as $agent): ?>
|
||||
<div class="agent-item mb-4 pb-3 border-bottom">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="flex-grow-1">
|
||||
<i class="fas fa-robot me-2 text-secondary"></i>
|
||||
<strong>Agent ID:</strong> <?= htmlspecialchars($agent['id']) ?> |
|
||||
<strong>Type:</strong> <?= htmlspecialchars($agent['agent_type_id']) ?> (<?= htmlspecialchars($agent['agent_description']) ?>) |
|
||||
<strong>Endpoint:</strong> <?= htmlspecialchars($agent['url']) ?><?= htmlspecialchars($agent['agent_endpoint']) ?>
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?page=settings#platform-<?= htmlspecialchars($platform_id) ?>agent-<?= htmlspecialchars($agent['id']) ?>" class="text-decoration-none">
|
||||
<i class="fas fa-edit ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-status"
|
||||
class="btn btn-primary"
|
||||
data-toggle="tooltip"
|
||||
data-trigger="hover"
|
||||
data-placement="bottom"
|
||||
title="Get the agent status"
|
||||
onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '/status', '<?= htmlspecialchars($agentTokens[$agent['id']]) ?>', true)">
|
||||
Get Status
|
||||
</button>
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-fetch"
|
||||
class="btn btn-primary"
|
||||
data-toggle="tooltip"
|
||||
data-trigger="hover"
|
||||
data-placement="bottom"
|
||||
title="Get data from the agent"
|
||||
onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '<?= htmlspecialchars($agent['agent_endpoint']) ?>', '<?= htmlspecialchars($agentTokens[$agent['id']]) ?>', <?= isset($_SESSION["agent{$agent['id']}_cache"]) ? 'true' : 'false' ?>)">
|
||||
Fetch Data
|
||||
</button>
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-cache"
|
||||
<?= !isset($_SESSION["agent{$agent['id']}_cache"]) ? 'style="display:none;" ' : '' ?>
|
||||
class="btn btn-secondary"
|
||||
data-toggle="tooltip"
|
||||
data-trigger="hover"
|
||||
data-placement="bottom"
|
||||
title="Load cache"
|
||||
onclick="loadCache('<?= htmlspecialchars($agent['id']) ?>')">
|
||||
Load Cache
|
||||
</button>
|
||||
<button id="agent<?= htmlspecialchars($agent['id']) ?>-clear"
|
||||
<?= !isset($_SESSION["agent{$agent['id']}_cache"]) ? 'style="display:none;" ' : '' ?>
|
||||
class="btn btn-danger"
|
||||
data-toggle="tooltip"
|
||||
data-trigger="hover"
|
||||
data-placement="bottom"
|
||||
title="Clear cache"
|
||||
onclick="clearCache('<?= htmlspecialchars($agent['id']) ?>')">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
<span id="cacheInfo<?= htmlspecialchars($agent['id']) ?>" class="ms-2 <?= isset($_SESSION["agent{$agent['id']}_cache"]) ? '' : 'd-none' ?>"></span>
|
||||
<pre class="results mt-3" id="result<?= htmlspecialchars($agent['id']) ?>">Click a button to display data from the agent.</pre>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- agents live data -->
|
|
@ -1,110 +0,0 @@
|
|||
|
||||
<!-- jitsi components events -->
|
||||
<div class="container-fluid mt-2">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6 mb-5">
|
||||
<h2 class="mb-0">Jitsi components events</h2>
|
||||
<small>log events related to Jitsi Meet components like Jicofo, Videobridge, Jigasi, etc.</small>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
|
||||
<!-- component events 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="components">
|
||||
<div class="col-md-auto">
|
||||
<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-auto">
|
||||
<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>
|
||||
<div class="col-md-2">
|
||||
<label for="name" class="form-label">Component name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($_REQUEST['name'] ?? '') ?>" placeholder="Component name">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control" id="id" name="id" value="<?= htmlspecialchars($_REQUEST['id'] ?? '') ?>" placeholder="Search in component IDs">
|
||||
<input type="text" class="form-control" id="event" name="event" value="<?= htmlspecialchars($_REQUEST['event'] ?? '') ?>" placeholder="Search in event messages">
|
||||
</div>
|
||||
<div class="col-md-auto align-middle">
|
||||
<button type="submit" class="btn btn-primary me-2">
|
||||
<i class="fas fa-search me-2"></i>Search
|
||||
</button>
|
||||
<a href="?page=components" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-2"></i>Clear
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /component events filter -->
|
||||
|
||||
<!-- component events -->
|
||||
<?php if ($time_range_specified || count($filterMessage)) { ?>
|
||||
<div class="alert alert-info m-0 mb-3 small">
|
||||
<?php if ($time_range_specified) { ?>
|
||||
<p class="mb-0"><i class="fas fa-calendar-alt me-2"></i>Time period:
|
||||
<strong>
|
||||
<?= $from_time == '0000-01-01' ? 'beginning' : date('d M Y', strtotime($from_time)) ?> - <?= $until_time == '9999-12-31' ? 'now' : date('d M Y', strtotime($until_time)) ?>
|
||||
</strong>
|
||||
</p>
|
||||
<?php } ?>
|
||||
<?php if (count($filterMessage)) {
|
||||
foreach ($filterMessage as $message) { ?>
|
||||
<p class="mb-0"><i class="fas fa-users me-2"></i><?= $message ?></strong></p>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
|
||||
<div class="mb-5">
|
||||
<?php if (!empty($components['records'])) { ?>
|
||||
<div class="table-responsive border">
|
||||
<table class="table table-results table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>component</th>
|
||||
<th>log level</th>
|
||||
<th>time</th>
|
||||
<th>component ID</th>
|
||||
<th>event</th>
|
||||
<th>parameter</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($components['records'] as $row) { ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&name=<?= htmlspecialchars($row['component'] ?? '') ?>">
|
||||
<?= htmlspecialchars($row['component'] ?? '') ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($row['loglevel']) ?></td>
|
||||
<td><span class="text-muted"><?= date('d M Y H:i:s', strtotime($row['time'])) ?></span></td>
|
||||
<td>
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&id=<?= htmlspecialchars($row['component ID'] ?? '') ?>">
|
||||
<?= htmlspecialchars($row['component ID'] ?? '') ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($row['event']) ?></td>
|
||||
<td><?= htmlspecialchars($row['param']) ?></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php include '../app/templates/pagination.php'; ?>
|
||||
<?php } else { ?>
|
||||
<div class="alert alert-danger m-0">
|
||||
<i class="fas fa-info-circle me-2"></i>No component events found for the specified criteria.
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /jitsi components events -->
|
|
@ -1,145 +0,0 @@
|
|||
|
||||
<!-- jitsi conferences events -->
|
||||
<div class="container-fluid mt-2">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6 mb-5">
|
||||
<h2 class="mb-0">Jitsi conferences events</h2>
|
||||
<small>log events related to conferences in Jitsi Meet</small>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
|
||||
<!-- conference events 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="conferences">
|
||||
<div class="col-md-auto">
|
||||
<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-auto">
|
||||
<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>
|
||||
<div class="col-md-2">
|
||||
<label for="name" class="form-label">Conference ID</label>
|
||||
<input type="text" class="form-control" id="id" name="name" value="<?= htmlspecialchars($_REQUEST['id'] ?? '') ?>" placeholder="Conference ID">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="name" class="form-label">Conference name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($_REQUEST['name'] ?? '') ?>" placeholder="Search in conference names">
|
||||
</div>
|
||||
<div class="col-md-auto align-middle">
|
||||
<button type="submit" class="btn btn-primary me-2">
|
||||
<i class="fas fa-search me-2"></i>Search
|
||||
</button>
|
||||
<a href="?page=conferences" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-2"></i>Clear
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /conference events filter -->
|
||||
|
||||
<!-- conference events -->
|
||||
<?php if ($time_range_specified || count($filterMessage)) { ?>
|
||||
<div class="alert alert-info m-0 mb-3 small">
|
||||
<?php if ($time_range_specified) { ?>
|
||||
<p class="mb-0"><i class="fas fa-calendar-alt me-2"></i>Time period:
|
||||
<strong>
|
||||
<?= $from_time == '0000-01-01' ? 'beginning' : date('d M Y', strtotime($from_time)) ?> - <?= $until_time == '9999-12-31' ? 'now' : date('d M Y', strtotime($until_time)) ?>
|
||||
</strong>
|
||||
</p>
|
||||
<?php } ?>
|
||||
<?php if (count($filterMessage)) {
|
||||
foreach ($filterMessage as $message) { ?>
|
||||
<p class="mb-0"><i class="fas fa-users me-2"></i><?= $message ?></strong></p>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="mb-5">
|
||||
<?php if (!empty($conferences['records'])) { ?>
|
||||
<div class="table-responsive border">
|
||||
<table class="table table-results table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<?php foreach (array_keys($conferences['records'][0]) as $header) { ?>
|
||||
<th scope="col" class="text-nowrap"><?= htmlspecialchars($header) ?></th>
|
||||
<?php } ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($conferences['records'] as $row) { ?>
|
||||
<tr>
|
||||
<?php foreach ($row as $key => $column) {
|
||||
if ($key === 'conference ID' && isset($conferenceId) && $conferenceId === $column) { ?>
|
||||
<td class="text-nowrap">
|
||||
<strong <?= (strlen($column ?? '') > 20) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
|
||||
<?= htmlspecialchars(strlen($column ?? '') > 20 ? substr($column, 0, 20) . '...' : $column ?? '') ?>
|
||||
</strong>
|
||||
</td>
|
||||
<?php } elseif ($key === 'conference ID') { ?>
|
||||
<td class="text-nowrap">
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=conferences&id=<?= htmlspecialchars($column ?? '') ?>"
|
||||
<?= (strlen($column ?? '') > 16) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
|
||||
<?= htmlspecialchars(strlen($column ?? '') > 16 ? substr($column, 0, 16) . '...' : $column ?? '') ?>
|
||||
</a>
|
||||
</td>
|
||||
<?php } elseif ($key === 'conference name' && isset($conferenceName) && $conferenceName === $column) { ?>
|
||||
<td class="text-nowrap">
|
||||
<strong <?= (strlen($column ?? '') > 20) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
|
||||
<?= htmlspecialchars(strlen($column ?? '') > 20 ? substr($column, 0, 20) . '...' : $column ?? '') ?>
|
||||
</strong>
|
||||
</td>
|
||||
<?php } elseif ($key === 'conference name') { ?>
|
||||
<td class="text-nowrap">
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=conferences&name=<?= htmlspecialchars($column ?? '') ?>"
|
||||
<?= (strlen($column ?? '') > 16) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
|
||||
<?= htmlspecialchars(strlen($column ?? '') > 16 ? substr($column, 0, 16) . '...' : $column ?? '') ?>
|
||||
</a>
|
||||
</td>
|
||||
<?php } elseif ($key === 'conference host') { ?>
|
||||
<td class="text-nowrap">
|
||||
<span <?= (strlen($column ?? '') > 30) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
|
||||
<?= htmlspecialchars(strlen($column ?? '') > 30 ? substr($column, 0, 30) . '...' : $column ?? '') ?>
|
||||
</span>
|
||||
</td>
|
||||
<?php } elseif ($key === 'participant ID') { ?>
|
||||
<td class="text-nowrap">
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=participants&id=<?= htmlspecialchars($column ?? '') ?>"
|
||||
<?= (strlen($column ?? '') > 16) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
|
||||
<?= htmlspecialchars(strlen($column ?? '') > 16 ? substr($column, 0, 16) . '...' : $column ?? '') ?>
|
||||
</a>
|
||||
</td>
|
||||
<?php } elseif ($key === 'component') { ?>
|
||||
<td class="text-nowrap">
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&name=<?= htmlspecialchars($column ?? '') ?>"
|
||||
<?= (strlen($column ?? '') > 16) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
|
||||
<?= htmlspecialchars(strlen($column ?? '') > 16 ? substr($column, 0, 16) . '...' : $column ?? '') ?>
|
||||
</a>
|
||||
</td>
|
||||
<?php } elseif ($key === 'time' || $key === 'start' || $key === 'end') { ?>
|
||||
<td class="text-nowrap"><?= !empty($column) ? date('d M Y H:i:s',strtotime($column)) : '<small class="text-muted">n/a</small>' ?></td>
|
||||
<?php } else { ?>
|
||||
<td><?= htmlspecialchars($column ?? '') ?></td>
|
||||
<?php }
|
||||
} ?>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php include '../app/templates/pagination.php'; ?>
|
||||
<?php } else { ?>
|
||||
<div class="alert alert-danger m-0">
|
||||
<i class="fas fa-info-circle me-2"></i>No conference events found for the specified criteria.
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /jitsi conferences events -->
|
|
@ -0,0 +1,72 @@
|
|||
|
||||
<!-- widget "agents" -->
|
||||
<div class="card text-center w-50 mx-auto">
|
||||
<p class="h4 card-header">Add new Jilo Agent to Jitsi platform "<strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong>"</p>
|
||||
<div class="card-body">
|
||||
<!--p class="card-text">add new agent:</p-->
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="type" class="form-label">type</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<select class="form-control" type="text" name="type" id="agent_type_id" required>
|
||||
<option></option>
|
||||
<?php foreach ($jilo_agent_types as $agent_type) { ?>
|
||||
<option value="<?= htmlspecialchars($agent_type['id']) ?>"<?php
|
||||
if (in_array($agent_type['id'], $jilo_agent_types_in_platform)) {
|
||||
echo 'disabled="disabled"';
|
||||
} ?>>
|
||||
<?= htmlspecialchars($agent_type['description']) ?>
|
||||
</option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
<p class="text-start"><small>type of agent (meet, jvb, jibri, etc.)<br />if a type has already been aded, it's disabled here</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="url" class="form-label">URL</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="url" value="https://" required />
|
||||
<p class="text-start"><small>URL of the Jilo Agent API (https://example.com:8081)</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="secret_key" class="form-label">secret key</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="secret_key" value="" required />
|
||||
<p class="text-start"><small>secret key for generating the access JWT token</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="check_period" class="form-label">check period</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="check_period" value="0" required />
|
||||
<p class="text-start"><small>period in minutes for the automatic agent check (0 disables it)</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="new" value="true" />
|
||||
<input type="hidden" name="item" value="agent" />
|
||||
|
||||
<br />
|
||||
<a class="btn btn-secondary" href="<?= htmlspecialchars($app_root) ?>?page=config" />Cancel</a>
|
||||
<input type="submit" class="btn btn-primary" value="Save" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "agents" -->
|
|
@ -0,0 +1,27 @@
|
|||
|
||||
<!-- widget "config file" -->
|
||||
<div class="card text-center w-75 mx-lef">
|
||||
<p class="h4 card-header">Jilo configuration file :: edit</p>
|
||||
<div class="card-body">
|
||||
<div class="card-text">
|
||||
<p class="text-danger"><strong>this may break everything, use with extreme caution</strong></p>
|
||||
</div>
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file">
|
||||
|
||||
<?php
|
||||
include '../app/helpers/render.php';
|
||||
editConfig($config, '0');
|
||||
echo "\n";
|
||||
?>
|
||||
|
||||
<p class="text-danger"><strong>this may break everything, use with extreme caution</strong></p>
|
||||
<br />
|
||||
<input type="hidden" name="item" value="config_file" />
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file" />Cancel</a>
|
||||
|
||||
<input type="submit" class="btn btn-danger btn-sm" value="Save" />
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "config file" -->
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
<!-- widget "config file" -->
|
||||
<div class="card text-center w-75 mx-lef">
|
||||
<p class="h4 card-header">Jilo configuration file</p>
|
||||
<div class="card-body">
|
||||
|
||||
<?php
|
||||
include '../app/helpers/render.php';
|
||||
renderConfig($config, '0');
|
||||
echo "\n";
|
||||
?>
|
||||
|
||||
<br />
|
||||
<a class="btn btn-outline-danger btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file&action=edit" />Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "config file" -->
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
<!-- widget "agents" -->
|
||||
<div class="card text-center w-50 mx-auto">
|
||||
<p class="h4 card-header">Jilo Agent configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p>
|
||||
<div class="card-body">
|
||||
<p class="card-text">delete an agent:</p>
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config">
|
||||
<?php
|
||||
foreach ($agentDetails[0] as $key => $value) {
|
||||
// if ($key === 'id') continue;
|
||||
?>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="<?= htmlspecialchars($key) ?>" class="form-label"><?= htmlspecialchars($key) ?>:</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="text-start"><?= htmlspecialchars($value ?? '') ?></div>
|
||||
<input type="hidden" name="<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value ?? '') ?>" />
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<br />
|
||||
<input type="hidden" name="agent" value="<?= htmlspecialchars($agentDetails[0]['id']) ?>" />
|
||||
<input type="hidden" name="delete" value="true" />
|
||||
<p class="h5 text-danger">Are you sure you want to delete this agent?</p>
|
||||
<br />
|
||||
<a class="btn btn-secondary" href="<?= htmlspecialchars($app_root) ?>?page=config#platform<?= htmlspecialchars($platform_id) ?>agent<?= htmlspecialchars($agentDetails[0]['id']) ?>" />Cancel</a>
|
||||
<input type="submit" class="btn btn-danger" value="Delete" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "agents" -->
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
<!-- agents -->
|
||||
<div class="card text-center w-50 mx-auto">
|
||||
<p class="h4 card-header">Jilo Agent configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p>
|
||||
<div class="card-body">
|
||||
<p class="card-text">edit the agent details:</p>
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="type_id" class="form-label">type</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<select class="form-control" type="text" name="type" id="agent_type_id" required>
|
||||
<option></option>
|
||||
<?php foreach ($jilo_agent_types as $agent_type) { ?>
|
||||
<option value="<?= htmlspecialchars($agent_type['id']) ?>" <?php if ($agentDetails[0]['agent_type_id'] === $agent_type['id']) echo 'selected'; ?>>
|
||||
<?= htmlspecialchars($agent_type['description']) ?>
|
||||
</option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
<p class="text-start"><small>type of agent (meet, jvb, jibri, all)</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="url" class="form-label">URL</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="url" value="<?= htmlspecialchars($agentDetails[0]['url']) ?>" required />
|
||||
<p class="text-start"><small>URL of the Jilo Agent API (https://example.com:8081)</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="secret_key" class="form-label">secret key</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="secret_key" value="<?= htmlspecialchars($agentDetails[0]['secret_key']) ?>" required />
|
||||
<p class="text-start"><small>secret key for generating the access JWT token</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="check_period" class="form-label">check period</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="check_period" value="<?= htmlspecialchars($agentDetails[0]['check_period']) ?>" required />
|
||||
<p class="text-start"><small>period in minutes for the automatic agent check (0 disables it)</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<br />
|
||||
<input type="hidden" name="agent" value="<?= htmlspecialchars($agentDetails[0]['id']) ?>" />
|
||||
<a class="btn btn-secondary" href="<?= htmlspecialchars($app_root) ?>?page=config#platform<?= htmlspecialchars($platform_id) ?>agent<?= htmlspecialchars($agentDetails[0]['id']) ?>" />Cancel</a>
|
||||
<input type="submit" class="btn btn-primary" value="Save" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /agents -->
|
|
@ -0,0 +1,50 @@
|
|||
|
||||
<!-- widget "hosts" -->
|
||||
<div class="card text-center w-50 mx-lef">
|
||||
<p class="h4 card-header">Add new host in Jitsi platform <strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong></p>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=host">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="address" class="form-label">address</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="address" value="" required autofocus />
|
||||
<p class="text-start"><small>DNS name or IP address of the machine</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="port" class="form-label">port</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="port" value="" required />
|
||||
<p class="text-start"><small>port on which the Jilo Agent is listening</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="name" class="form-label">name</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="name" value="" />
|
||||
<p class="text-start"><small>description or name of the host (optional)</small></p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="platform" value="<?= htmlspecialchars($platformDetails[0]['id'])?>" />
|
||||
<input type="hidden" name="item" value="host" />
|
||||
<input type="hidden" name="new" value="true" />
|
||||
|
||||
<br />
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_id) ?>&host=<?= htmlspecialchars($host) ?>#platform<?= htmlspecialchars($platform_id) ?>host<?= htmlspecialchars($host) ?>" />Cancel</a>
|
||||
|
||||
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "hosts" -->
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
<!-- widget "hosts" -->
|
||||
<div class="card text-center w-50 mx-lef">
|
||||
<p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p>
|
||||
<div class="card-body">
|
||||
<p class="card-text">delete a host:</p>
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=host">
|
||||
<?php
|
||||
foreach ($hostDetails[0] as $key => $value) {
|
||||
?>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="<?= htmlspecialchars($key) ?>" class="form-label"><?= htmlspecialchars($key) ?>:</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="text-start"><?= htmlspecialchars($value ?? '') ?></div>
|
||||
<input type="hidden" name="<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value ?? '') ?>" />
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<br />
|
||||
<input type="hidden" name="host" value="<?= htmlspecialchars($hostDetails[0]['id']) ?>" />
|
||||
<input type="hidden" name="delete" value="true" />
|
||||
<p class="h5 text-danger">Are you sure you want to delete this host?</p>
|
||||
<br />
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_id) ?>&host=<?= htmlspecialchars($host) ?>#platform<?= htmlspecialchars($platform_id) ?>host<?= htmlspecialchars($host) ?>" />Cancel</a>
|
||||
|
||||
<input type="submit" class="btn btn-danger btn-sm" value="Delete" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "hosts" -->
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
<!-- widget "hosts" -->
|
||||
<div class="card text-center w-50 mx-lef">
|
||||
<p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p>
|
||||
<div class="card-body">
|
||||
<p class="card-text">edit host details:</p>
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=host">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="address" class="form-label">address</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="address" value="<?= htmlspecialchars($hostDetails[0]['address'] ?? '') ?>" required autofocus />
|
||||
<p class="text-start"><small>DNS name or IP address of the machine</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="port" class="form-label">port</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="port" value="<?= htmlspecialchars($hostDetails[0]['port'] ?? '') ?>" required />
|
||||
<p class="text-start"><small>port on which the Jilo Agent is listening</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="name" class="form-label">name</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="name" value="<?= htmlspecialchars($hostDetails[0]['name'] ?? '') ?>" />
|
||||
<p class="text-start"><small>description or name of the host (optional)</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="platform" value="<?= htmlspecialchars($platform_id) ?>" />
|
||||
<input type="hidden" name="item" value="host" />
|
||||
<input type="hidden" name="host" value="<?= htmlspecialchars($hostDetails[0]['id']) ?>" />
|
||||
|
||||
<br />
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_id) ?>&host=<?= htmlspecialchars($host) ?>#platform<?= htmlspecialchars($platform_id) ?>host<?= htmlspecialchars($host) ?>" />Cancel</a>
|
||||
|
||||
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "hosts" -->
|
|
@ -0,0 +1,39 @@
|
|||
|
||||
<!-- widget "hosts" -->
|
||||
<div class="card text-center w-75 mx-lef">
|
||||
<p class="h4 card-header">Jilo configuration :: Jitsi Meet hosts</p>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Jitsi hosts configuration
|
||||
</p>
|
||||
<?php foreach ($platformsAll as $platform_array) {
|
||||
$hosts = $hostObject->getHostDetails($platform_array['id']);
|
||||
?>
|
||||
<a name="platform<?= htmlspecialchars($platform_array['id']) ?>"></a>
|
||||
<div class="row mb-1 border <?= isset($_REQUEST['platform']) && (int)$platform_array['id'] === (int)$_REQUEST['platform'] ? 'rounded bg-light' : '' ?>" style="padding: 20px; padding-bottom: 0px;">
|
||||
<p class="text-start">
|
||||
platform <strong><?= htmlspecialchars($platform_array['name']) ?></strong>
|
||||
</p>
|
||||
<ul class="text-start" style="padding-left: 50px;">
|
||||
<?php foreach ($hosts as $host_array) { ?>
|
||||
<li style="padding-bottom: 10px;">
|
||||
<a name="platform<?= htmlspecialchars($platform_array['id']) ?>host<?= htmlspecialchars($host_array['id']) ?>"></a>
|
||||
<span class="<?= isset($_REQUEST['platform']) && (int)$platform_array['id'] === (int)$_REQUEST['platform'] && isset($_REQUEST['host']) && (int)$host_array['id'] === (int)$_REQUEST['host'] ? 'border rounded bg-light' : '' ?>" style="padding: 10px;">
|
||||
<?= htmlspecialchars($host_array['address']) ?>:<?= htmlspecialchars($host_array['port']) ?>
|
||||
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($host_array['platform_id']) ?>&host=<?= htmlspecialchars($host_array['id']) ?>&action=edit">edit host</a>
|
||||
<a class="btn btn-outline-danger btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($host_array['platform_id']) ?>&host=<?= htmlspecialchars($host_array['id']) ?>&action=delete">delete host</a>
|
||||
</span>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<p class="text-start" style="padding-left: 50px;">
|
||||
total <?= htmlspecialchars(count($hosts)) ?> jilo <?= htmlspecialchars(count($hosts)) === '1' ? 'host' : 'hosts' ?>
|
||||
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=add">add new</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "hosts" -->
|
|
@ -0,0 +1,134 @@
|
|||
|
||||
<!-- widget "config" -->
|
||||
<div class="card text-center w-75 mx-lef">
|
||||
<p class="h4 card-header">Jilo configuration</p>
|
||||
<p class="h6 card-header">
|
||||
<span class="btn btn-outline-primary btn-sm active" aria-pressed="true" style="cursor: default;">platforms</span>
|
||||
<a href="" class="btn btn-outline-primary btn-sm">hosts</a>
|
||||
<a href="" class="btn btn-outline-primary btn-sm">endpoints</a>
|
||||
|
||||
<a href="" class="btn btn-outline-primary btn-sm">config file</a>
|
||||
</p>
|
||||
<div class="card-body">
|
||||
<p class="card-text">main variables</p>
|
||||
<?php
|
||||
include '../app/helpers/render.php';
|
||||
renderConfig($config, '0');
|
||||
echo "\n";
|
||||
?>
|
||||
|
||||
<hr />
|
||||
<p class="card-text">platforms configuration <a class="btn btn-secondary" style="padding: 0px;" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&action=add">add new</a></p>
|
||||
|
||||
<?php foreach ($platformsAll as $platform_array) {
|
||||
$agents = $agentObject->getAgentDetails($platform_array['id']);
|
||||
?>
|
||||
|
||||
<a name="platform<?= htmlspecialchars($platform_array['id']) ?>"></a>
|
||||
<div class="row mb-3" style="padding-left: 0px;">
|
||||
<div class="border bg-light" style="padding-left: 50px; padding-bottom: 0px; padding-top: 0px;">
|
||||
<a style="text-decoration: none;" data-toggle="collapse" href="#collapsePlatform<?= htmlspecialchars($platform_array['id']) ?>" role="button" aria-expanded="true" aria-controls="collapsePlatform<?= htmlspecialchars($platform_array['id']) ?>">
|
||||
<div class="border bg-white text-start mb-3 rounded mt-3" data-toggle="tooltip" data-placement="bottom" title="configuration for platform <?= htmlspecialchars($platform_array['id']) ?>">
|
||||
<i class="fas fa-wrench"></i>
|
||||
<small>platform <?= htmlspecialchars($platform_array['id']) ?> (<?= htmlspecialchars($platform_array['name']) ?>)</small>
|
||||
</div>
|
||||
</a>
|
||||
<div class="collapse show" id="collapsePlatform<?= htmlspecialchars($platform_array['id']) ?>">
|
||||
|
||||
<div class="row mb-1" style="padding-left: 0px;">
|
||||
<div class="col-md-8 text-start">
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col-md-8 text-start">
|
||||
<a class="btn btn-secondary" style="padding: 2px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=edit">edit platform</a>
|
||||
<?php if (count($platformsAll) <= 1) { ?>
|
||||
<span class="btn btn-light" style="padding: 2px;" href="#" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="can't delete the last platform">delete platform</span>
|
||||
<?php } else { ?>
|
||||
<a class="btn btn-danger" style="padding: 2px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=delete">delete platform</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div style="padding-left: 100px; padding-bottom: 20px;">
|
||||
<?php foreach ($platform_array as $key => $value) {
|
||||
if ($key === 'id') continue;
|
||||
?>
|
||||
<div class="row mb-1" style="padding-left: 100px;">
|
||||
<div class="col-md-4 text-end">
|
||||
<?= htmlspecialchars($key) ?>:
|
||||
</div>
|
||||
<div class="border col-md-8 text-start">
|
||||
<?= htmlspecialchars($value) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
<hr />
|
||||
<p class="card-text">jilo agents on platform <?= htmlspecialchars($platform_array['id']) ?> (<?= htmlspecialchars($platform_array['name']) ?>)
|
||||
<br />
|
||||
total <?= htmlspecialchars(count($agents)) ?> <?= htmlspecialchars(count($agents)) === 1 ? 'jilo agent' : 'jilo agents' ?>
|
||||
<a class="btn btn-secondary" style="padding: 0px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=add-agent">
|
||||
add new
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<?php foreach ($agents as $agent_array) { ?>
|
||||
|
||||
<a name="platform<?= htmlspecialchars($platform_array['id']) ?>agent<?= htmlspecialchars($agent_array['id']) ?>"></a>
|
||||
<div class="row mb-3" style="padding-left: 0px;">
|
||||
<div class="border rounded bg-light" style="padding-left: 50px; padding-bottom: 20px; padding-top: 20px;">
|
||||
<div class="row mb-1" style="padding-left: 0px;">
|
||||
<div class="col-md-4 text-end">
|
||||
agent id <?= htmlspecialchars($agent_array['id']) ?>:
|
||||
</div>
|
||||
<div class="col-md-8 text-start">
|
||||
<a class="btn btn-secondary" style="padding: 2px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($agent_array['platform_id']) ?>&agent=<?= htmlspecialchars($agent_array['id']) ?>&action=edit">edit agent</a>
|
||||
<a class="btn btn-danger" style="padding: 2px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($agent_array['platform_id']) ?>&agent=<?= htmlspecialchars($agent_array['id']) ?>&action=delete">delete agent</a>
|
||||
</div>
|
||||
<div style="padding-left: 100px; padding-bottom: 20px;">
|
||||
<div class="row mb-1" style="padding-left: 100px;">
|
||||
<div class="col-md-4 text-end">
|
||||
agent type:
|
||||
</div>
|
||||
<div class="border col-md-8 text-start">
|
||||
<?= htmlspecialchars($agent_array['agent_description']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1" style="padding-left: 100px;">
|
||||
<div class="col-md-4 text-end">
|
||||
endpoint:
|
||||
</div>
|
||||
<div class="border col-md-8 text-start">
|
||||
<?= htmlspecialchars($agent_array['url'].$agent_array['agent_endpoint']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (isset($agent_array['check_period']) && $agent_array['check_period'] !== 0) { ?>
|
||||
<div class="row mb-1" style="padding-left: 100px;">
|
||||
<div class="col-md-4 text-end">
|
||||
check period:
|
||||
</div>
|
||||
<div class="border col-md-8 text-start">
|
||||
<?= htmlspecialchars($agent_array['check_period']) ?> <?= ($agent_array['check_period'] == 1 ? 'minute' : 'minutes') ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "config" -->
|
|
@ -0,0 +1,51 @@
|
|||
|
||||
<!-- widget "platforms" -->
|
||||
<div class="card text-center w-50 mx-lef">
|
||||
<p class="h4 card-header">Add new Jitsi platform</p>
|
||||
<div class="card-body">
|
||||
<!--p class="card-text">add new platform:</p-->
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config&item=platform">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="name" class="form-label">name</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="name" value="" required autofocus />
|
||||
<p class="text-start"><small>descriptive name for the platform</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="jitsi_url" class="form-label">Jitsi URL</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="jitsi_url" value="https://" required />
|
||||
<p class="text-start"><small>URL of the Jitsi Meet (used for checks and for loading config.js)</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="jilo_database" class="form-label">jilo_database</label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="jilo_database" value="" required />
|
||||
<p class="text-start"><small>path to the database file (relative to the app root)</small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="new" value="true" />
|
||||
|
||||
<br />
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform" />Cancel</a>
|
||||
|
||||
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "platforms" -->
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
<!-- widget "platforms" -->
|
||||
<div class="card text-center w-50 mx-lef">
|
||||
<p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong> :: delete</p>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config">
|
||||
<?php
|
||||
foreach ($platformDetails[0] as $key => $value) {
|
||||
if ($key === 'id') continue;
|
||||
?>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="<?= htmlspecialchars($key) ?>" class="form-label"><?= htmlspecialchars($key) ?>:</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="text-start"><?= htmlspecialchars($value) ?? '' ?></div>
|
||||
<input type="hidden" name="<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value ?? '')?>" />
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<br />
|
||||
<input type="hidden" name="platform" value="<?= htmlspecialchars($platform_id) ?>" />
|
||||
<input type="hidden" name="delete" value="true" />
|
||||
<p class="h5 text-danger">Are you sure you want to delete this platform?</p>
|
||||
<br />
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&platform=<?= htmlspecialchars($platform_id) ?>#platform<?= htmlspecialchars($platform_id) ?>" />Cancel</a>
|
||||
|
||||
<input type="submit" class="btn btn-danger btn-sm" value="Delete" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "platforms" -->
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
<!-- widget "platforms" -->
|
||||
<div class="card text-center w-50 mx-lef">
|
||||
<p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong> :: edit</p>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config&item=platform">
|
||||
<?php
|
||||
foreach ($platformDetails[0] as $key => $value) {
|
||||
if ($key === 'id') continue;
|
||||
?>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4 text-end">
|
||||
<label for="<?= htmlspecialchars($config_item) ?>" class="form-label"><?= htmlspecialchars($key) ?></label>
|
||||
<span class="text-danger" style="margin-right: -12px;">*</span>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<input class="form-control" type="text" name="<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value ?? '') ?>" required autofocus />
|
||||
<?php if ($key === 'name') { ?>
|
||||
<p class="text-start"><small>descriptive name for the platform</small></p>
|
||||
<?php } elseif ($key === 'jitsi_url') { ?>
|
||||
<p class="text-start"><small>URL of the Jitsi Meet (used for checks and for loading config.js)</small></p>
|
||||
<?php } elseif ($key === 'jilo_database') { ?>
|
||||
<p class="text-start"><small>path to the database file (relative to the app root)</small></p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<br />
|
||||
<input type="hidden" name="platform" value="<?= htmlspecialchars($platform_id) ?>" />
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&platform=<?= htmlspecialchars($platform_id) ?>#platform<?= htmlspecialchars($platform_id) ?>" />Cancel</a>
|
||||
|
||||
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "platforms" -->
|
|
@ -0,0 +1,60 @@
|
|||
|
||||
<!-- widget "platforms" -->
|
||||
<div class="card text-center w-75 mx-lef">
|
||||
<p class="h4 card-header">Jilo configuration :: Jitsi Meet platforms</p>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Jitsi platforms configuration <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&action=add">add new</a></p>
|
||||
<?php foreach ($platformsAll as $platform_array) {
|
||||
$hosts = $hostObject->getHostDetails($platform_array['id']);
|
||||
$agents = $agentObject->getAgentDetails($platform_array['id']);
|
||||
?>
|
||||
<a name="platform<?= htmlspecialchars($platform_array['id']) ?>"></a>
|
||||
<div class="row mb-1 border<?= isset($_REQUEST['platform']) && (int)$platform_array['id'] === (int)$_REQUEST['platform'] ? ' bg-light' : '' ?>" style="padding: 20px; padding-bottom: 0px;">
|
||||
<p>
|
||||
platform id <?= htmlspecialchars($platform_array['id']) ?> - <strong><?= htmlspecialchars($platform_array['name']) ?></strong>
|
||||
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=edit">edit platform</a>
|
||||
<?php if (count($platformsAll) <= 1) { ?>
|
||||
<span class="btn btn-outline-light btn-sm" href="#" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="can't delete the last platform">delete platform</span>
|
||||
<?php } else { ?>
|
||||
<a class="btn btn-outline-danger btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=delete">delete platform</a>
|
||||
<?php } ?>
|
||||
</p>
|
||||
<div style="padding-left: 100px; padding-bottom: 20px;">
|
||||
<?php foreach ($platform_array as $key => $value) {
|
||||
if ($key === 'id') continue;
|
||||
?>
|
||||
<div class="row mb-1" style="padding-left: 100px;">
|
||||
<div class="col-md-4 text-end">
|
||||
<?= htmlspecialchars($key) ?>:
|
||||
</div>
|
||||
<div class="col-md-8 text-start">
|
||||
<?php if ($key === 'jitsi_url') { ?>
|
||||
<a href="<?= htmlspecialchars($value) ?>" target="_blank" rel="noopener noreferrer" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="open the Jitsi Meet platform in a new window">
|
||||
<?= htmlspecialchars($value) ?>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<?= htmlspecialchars($value) ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="row mb-1" style="padding-left: 100px;">
|
||||
<div class="col-md-4 text-end"></div>
|
||||
<div class="col-md-8 text-start">
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_array['id']) ?>#platform<?= htmlspecialchars($platform_array['id']) ?>"><?= htmlspecialchars(count($hosts)) ?> <?= htmlspecialchars(count($hosts)) === '1' ? 'host' : 'hosts' ?></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1" style="padding-left: 100px;">
|
||||
<div class="col-md-4 text-end"></div>
|
||||
<div class="col-md-8 text-start">
|
||||
<a href="<?= htmlspecialchars($app_root) ?>?page=config&item=endpoint&platform=<?= htmlspecialchars($platform_array['id']) ?>#platform<?= htmlspecialchars($platform_array['id']) ?>"><?= htmlspecialchars(count($agents)) ?> <?= htmlspecialchars(count($agents)) === '1' ? 'endpoint' : 'endpoints' ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "platforms" -->
|
|
@ -1,198 +0,0 @@
|
|||
|
||||
<!-- config file -->
|
||||
<div class="container-fluid mt-2">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12 mb-4">
|
||||
<h2>Configuration</h2>
|
||||
<small><?= htmlspecialchars($config['site_name']) ?> configuration file: <em><?= htmlspecialchars($localConfigPath) ?></em></small>
|
||||
<?php if ($configMessage) { ?>
|
||||
<?= $configMessage ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center py-3">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-wrench me-2 text-secondary"></i>
|
||||
<?= htmlspecialchars($config['site_name']) ?> app configuration
|
||||
</h5>
|
||||
<?php if ($userObject->hasRight($userId, 'superuser') ||
|
||||
$userObject->hasRight($userId, 'edit config file')) { ?>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm toggle-edit" <?= !$isWritable ? 'disabled' : '' ?>>
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</button>
|
||||
<div class="edit-controls d-none">
|
||||
<button type="button" class="btn btn-danger btn-sm save-config">
|
||||
<i class="fas fa-save me-2"></i>Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm ms-2 cancel-edit">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
<form id="configForm">
|
||||
<?php
|
||||
include CSRF_TOKEN_INCLUDE;
|
||||
|
||||
function renderConfigItem($key, $value, $path = '') {
|
||||
$fullPath = $path ? $path . '[' . $key . ']' : $key;
|
||||
// Only capitalize first letter, not every word
|
||||
$displayName = ucfirst(str_replace('_', ' ', $key));
|
||||
|
||||
if (is_array($value)) {
|
||||
echo "\t\t\t\t\t\t\t\t<div class=\"config-section mb-4\">";
|
||||
echo "\n\t\t\t\t\t\t\t\t\t<h6 class=\"border-bottom pb-2 mb-3\">" . htmlspecialchars($displayName) . '</h6>';
|
||||
echo "\n\t\t\t\t\t\t\t\t\t<div class=\"ps-4\">\n";
|
||||
foreach ($value as $subKey => $subValue) {
|
||||
renderConfigItem($subKey, $subValue, $fullPath);
|
||||
}
|
||||
echo "\t\t\t\t\t\t\t\t\t</div>\n";
|
||||
echo "\t\t\t\t\t\t\t\t</div>\n";
|
||||
} else {
|
||||
?>
|
||||
<div class="config-item row mb-3 align-items-center">
|
||||
<div class="col-md-4 text-end">
|
||||
<label class="form-label mb-0"><?= htmlspecialchars($displayName) ?></label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="view-mode">
|
||||
<?php if (is_bool($value) || $key === 'registration_enabled') { ?>
|
||||
<span class="badge <?= $value ? 'bg-success' : 'bg-secondary' ?>"><?= $value ? 'Enabled' : 'Disabled' ?></span>
|
||||
<?php } elseif ($key === 'environment') { ?>
|
||||
<span class="badge <?= $value === 'production' ? 'bg-danger' : 'bg-info' ?>"><?= htmlspecialchars($value) ?></span>
|
||||
<?php } else {
|
||||
if (empty($value) && $value !== '0') { ?>
|
||||
<span class="text-muted fst-italic">blank</span>
|
||||
<?php } else { ?>
|
||||
<span class="text-body"><?= htmlspecialchars($value) ?></span>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="edit-mode d-none">
|
||||
<?php if (is_bool($value) || $key === 'registration_enabled') { ?>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="<?= htmlspecialchars($fullPath) ?>" <?= $value ? 'checked' : '' ?>>
|
||||
</div>
|
||||
<?php } elseif ($key === 'environment') { ?>
|
||||
<select class="form-select form-select-sm" name="<?= htmlspecialchars($fullPath) ?>">
|
||||
<option value="development" <?= $value === 'development' ? 'selected' : '' ?>>development</option>
|
||||
<option value="production" <?= $value === 'production' ? 'selected' : '' ?>>production</option>
|
||||
</select>
|
||||
<?php } else { ?>
|
||||
<input type="text" class="form-control form-control-sm" name="<?= htmlspecialchars($fullPath) ?>" value="<?= htmlspecialchars($value) ?>">
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($config as $key => $value) {
|
||||
renderConfigItem($key, $value);
|
||||
} ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
// Toggle edit mode
|
||||
$('.toggle-edit').click(function() {
|
||||
$(this).hide();
|
||||
$('.edit-controls').removeClass('d-none');
|
||||
$('.view-mode').hide();
|
||||
$('.edit-mode').removeClass('d-none');
|
||||
});
|
||||
|
||||
// Cancel edit
|
||||
$('.cancel-edit').click(function() {
|
||||
$('.toggle-edit').show();
|
||||
$('.edit-controls').addClass('d-none');
|
||||
$('.view-mode').show();
|
||||
$('.edit-mode').addClass('d-none');
|
||||
});
|
||||
|
||||
// Save config
|
||||
$('.save-config').click(function() {
|
||||
const $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>Saving...');
|
||||
|
||||
// Build form data object
|
||||
const data = {};
|
||||
|
||||
// Handle text inputs
|
||||
$('#configForm input[type="text"]').each(function() {
|
||||
data[$(this).attr('name')] = $(this).val();
|
||||
});
|
||||
|
||||
// Handle checkboxes
|
||||
$('#configForm input[type="checkbox"]').each(function() {
|
||||
data[$(this).attr('name')] = $(this).prop('checked') ? '1' : '0';
|
||||
});
|
||||
|
||||
// Handle selects
|
||||
$('#configForm select').each(function() {
|
||||
data[$(this).attr('name')] = $(this).val();
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: '<?= htmlspecialchars($app_root) ?>?page=config',
|
||||
method: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-Token': $('input[name="csrf_token"]').val()
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
JsMessages.success(response.message || 'Config file updated successfully');
|
||||
|
||||
$('#configForm input[type="text"], #configForm input[type="checkbox"], #configForm select').each(function() {
|
||||
const $input = $(this);
|
||||
const $item = $input.closest('.config-item');
|
||||
const $viewMode = $item.find('.view-mode');
|
||||
|
||||
if ($item.length) {
|
||||
let value;
|
||||
if ($input.is('[type="checkbox"]')) {
|
||||
value = $input.prop('checked') ? '1' : '0';
|
||||
const isEnabled = value === '1';
|
||||
$viewMode.html(`<span class="badge ${isEnabled ? 'bg-success' : 'bg-secondary'}">${isEnabled ? 'Enabled' : 'Disabled'}</span>`);
|
||||
} else if ($input.is('select')) {
|
||||
value = $input.val();
|
||||
$viewMode.html(`<span class="badge ${value === 'production' ? 'bg-danger' : 'bg-info'}">${value}</span>`);
|
||||
} else {
|
||||
value = $input.val();
|
||||
$viewMode.html(value === '' ? '<span class="text-muted fst-italic">blank</span>' : `<span class="text-body">${value}</span>`);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Finally, exit edit mode
|
||||
$('.toggle-edit').show();
|
||||
$('.edit-controls').addClass('d-none');
|
||||
$('.view-mode').show();
|
||||
$('.edit-mode').addClass('d-none');
|
||||
} else {
|
||||
JsMessages.error(response.error || 'Error saving config');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
JsMessages.error('Error saving config: ' + error);
|
||||
},
|
||||
complete: function() {
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-save me-2"></i>Save');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!-- /config file -->
|
|
@ -1,108 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Two-factor authentication setup template
|
||||
*/
|
||||
?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Set up two-factor authentication</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<p>Two-factor authentication adds an extra layer of security to your account. Once enabled, you'll need to enter both your password and a code from your authenticator app when signing in.</p>
|
||||
</div>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?php echo htmlspecialchars($error); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($setupData) && is_array($setupData)): ?>
|
||||
<div class="setup-steps">
|
||||
<h4>1. Install an authenticator app</h4>
|
||||
<p>If you haven't already, install an authenticator app on your mobile device:</p>
|
||||
<ul>
|
||||
<li>Google Authenticator</li>
|
||||
<li>Microsoft Authenticator</li>
|
||||
<li>Authy</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="mt-4">2. Scan the QR code</h4>
|
||||
<p>Open your authenticator app and scan this QR code:</p>
|
||||
|
||||
<div class="text-center my-4">
|
||||
<div id="qrcode"></div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Can't scan? Use this code instead:</small><br>
|
||||
<code class="secret-key"><?php echo htmlspecialchars($setupData['secret']); ?></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">3. Verify setup</h4>
|
||||
<p>Enter the 6-digit code from your authenticator app to verify the setup:</p>
|
||||
|
||||
<form method="post" action="?page=credentials&item=2fa&action=setup" class="mt-3">
|
||||
<div class="form-group">
|
||||
<input type="text"
|
||||
name="code"
|
||||
class="form-control"
|
||||
pattern="[0-9]{6}"
|
||||
maxlength="6"
|
||||
required
|
||||
placeholder="Enter 6-digit code">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="secret" value="<?php echo htmlspecialchars($setupData['secret']); ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-3">
|
||||
Verify and enable 2FA
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4>Backup codes</h4>
|
||||
<p class="text-warning">
|
||||
<strong>Important:</strong> Save these backup codes in a secure place.
|
||||
If you lose access to your authenticator app, you can use these codes to sign in.
|
||||
Each code can only be used once.
|
||||
</p>
|
||||
<div class="backup-codes bg-light p-3 rounded">
|
||||
<?php foreach ($setupData['backupCodes'] as $code): ?>
|
||||
<code class="d-block"><?php echo htmlspecialchars($code); ?></code>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button class="btn btn-secondary mt-2" onclick="window.print()">
|
||||
Print backup codes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-danger">
|
||||
Failed to generate 2FA setup data. Please try again.
|
||||
</div>
|
||||
<a href="?page=credentials" class="btn btn-primary">Back to credentials</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (isset($setupData) && is_array($setupData)): ?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
new QRCode(document.getElementById("qrcode"), {
|
||||
text: <?php echo json_encode($setupData['otpauthUrl']); ?>,
|
||||
width: 200,
|
||||
height: 200
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
|
@ -1,86 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Two-factor authentication verification template
|
||||
*/
|
||||
?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Two-factor authentication</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?php echo htmlspecialchars($error); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p>Enter the 6-digit code from your authenticator app:</p>
|
||||
|
||||
<form method="post" action="?page=login&action=verify" class="mt-3">
|
||||
<div class="form-group">
|
||||
<input type="text"
|
||||
name="code"
|
||||
class="form-control form-control-lg text-center"
|
||||
pattern="[0-9]{6}"
|
||||
maxlength="6"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
required
|
||||
autofocus
|
||||
placeholder="000000">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="user_id" value="<?php echo htmlspecialchars($userId); ?>">
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block mt-4">
|
||||
Verify code
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-muted text-center">
|
||||
Lost access to your authenticator app?<br>
|
||||
<a href="#" data-toggle="collapse" data-target="#backupCodeForm">
|
||||
Use a backup code
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div class="collapse mt-3" id="backupCodeForm">
|
||||
<form method="post" action="?page=login&action=verify" class="mt-3">
|
||||
<div class="form-group">
|
||||
<label>Enter backup code:</label>
|
||||
<input type="text"
|
||||
name="backup_code"
|
||||
class="form-control"
|
||||
pattern="[a-f0-9]{8}"
|
||||
maxlength="8"
|
||||
required
|
||||
placeholder="Enter backup code">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="user_id" value="<?php echo htmlspecialchars($userId); ?>">
|
||||
|
||||
<button type="submit" class="btn btn-secondary btn-block">
|
||||
Use backup code
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-submit when 6 digits are entered
|
||||
document.querySelector('input[name="code"]').addEventListener('input', function(e) {
|
||||
if (e.target.value.length === 6 && e.target.checkValidity()) {
|
||||
e.target.form.submit();
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -1,100 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Combined credentials management template
|
||||
* Handles both password changes and 2FA management
|
||||
*/
|
||||
?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<!-- Password Management -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3>change password</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="?page=credentials&item=password">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="current_password">current password</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="current_password"
|
||||
name="current_password"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-3">
|
||||
<label for="new_password">new password</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="new_password"
|
||||
name="new_password"
|
||||
pattern=".{8,}"
|
||||
title="Password must be at least 8 characters long"
|
||||
required>
|
||||
<small class="form-text text-muted">minimum 8 characters</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-3">
|
||||
<label for="confirm_password">confirm new password</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
pattern=".{8,}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">change password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2FA Management -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>two-factor authentication</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-4">Two-factor authentication adds an extra layer of security to your account. Once enabled, you'll need to enter both your password and a code from your authenticator app when signing in.</p>
|
||||
|
||||
<?php if ($has2fa): ?>
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i> two-factor authentication is enabled
|
||||
</div>
|
||||
<form method="post" action="?page=credentials&item=2fa&action=disable">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to disable two-factor authentication? This will make your account less secure.')">
|
||||
disable two-factor authentication
|
||||
</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i> two-factor authentication is not enabled
|
||||
</div>
|
||||
<form method="post" action="?page=credentials&item=2fa&action=setup">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
set up two-factor authentication
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('confirm_password').addEventListener('input', function() {
|
||||
if (this.value !== document.getElementById('new_password').value) {
|
||||
this.setCustomValidity('Passwords do not match');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
}
|
||||
});</script>
|
|
@ -1,51 +0,0 @@
|
|||
|
||||
<!-- Two-Factor Authentication -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h3>Two-factor authentication</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($has2fa): ?>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<p class="mb-0">
|
||||
<i class="fas fa-shield-alt text-success"></i>
|
||||
Two-factor authentication is enabled
|
||||
</p>
|
||||
<small class="text-muted">
|
||||
Your account is protected with an authenticator app
|
||||
</small>
|
||||
</div>
|
||||
<form method="post" class="ml-3">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||
<input type="hidden" name="item" value="2fa">
|
||||
<input type="hidden" name="action" value="disable">
|
||||
<button type="submit" class="btn btn-outline-danger"
|
||||
onclick="return confirm('Are you sure you want to disable two-factor authentication? This will make your account less secure.')">
|
||||
Disable 2FA
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<p class="mb-0">
|
||||
<i class="fas fa-shield-alt text-muted"></i>
|
||||
Two-factor authentication is not enabled
|
||||
</p>
|
||||
<small class="text-muted">
|
||||
Add an extra layer of security to your account by requiring both your password and an authentication code
|
||||
</small>
|
||||
</div>
|
||||
<form method="post" class="ml-3">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
|
||||
<input type="hidden" name="item" value="2fa">
|
||||
<input type="hidden" name="action" value="enable">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Enable 2FA
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
<!-- widget "config.js" -->
|
||||
<div class="card text-center w-75 mx-lef">
|
||||
<p class="h4 card-header">Configuration of the Jitsi platform <strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong></p>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
<span class="m-3">URL: <?= htmlspecialchars($platformDetails[0]['jitsi_url']) ?></span>
|
||||
<span class="m-3">FILE: config.js</span>
|
||||
<?php if ($mode === 'raw') { ?>
|
||||
<span class="m-3"><a class="btn btn-light" href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=data&item=configjs">view only active lines</a></span>
|
||||
<?php } else { ?>
|
||||
<span class="m-3"><a class="btn btn-light" href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=data&item=configjs&mode=raw">view raw file contents</a></span>
|
||||
<?php } ?>
|
||||
</p>
|
||||
<pre class="results">
|
||||
<?php
|
||||
echo htmlspecialchars($platformConfigjs);
|
||||
?>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "config.js" -->
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
<!-- widget "interfaceconfig" -->
|
||||
<div class="card text-center w-75 mx-lef">
|
||||
<p class="h4 card-header">Configuration of the Jitsi platform <strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong></p>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
<span class="m-3">URL: <?= htmlspecialchars($platformDetails[0]['jitsi_url']) ?></span>
|
||||
<span class="m-3">FILE: interface_config.js</span>
|
||||
<?php if ($mode === 'raw') { ?>
|
||||
<span class="m-3"><a class="btn btn-light" href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=data&item=interfaceconfigjs">view only active lines</a></span>
|
||||
<?php } else { ?>
|
||||
<span class="m-3"><a class="btn btn-light" href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=data&item=interfaceconfigjs&mode=raw">view raw file contents</a></span>
|
||||
<?php } ?>
|
||||
</p>
|
||||
<pre class="results">
|
||||
<?php
|
||||
echo htmlspecialchars($platformInterfaceConfigjs);
|
||||
?>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "interfaceconfig" -->
|
|
@ -0,0 +1,76 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="card w-auto bg-light border-light card-body" style="flex-direction: row;"><?= $widget['title'] ?></div>
|
||||
|
||||
<!-- Results filter -->
|
||||
<div class="card w-auto bg-light border-light card-body text-right" style="text-align: right;">
|
||||
<form method="POST" id="filter_form" class="filter-results" action="?platform=<?= htmlspecialchars($platform_id) ?>&page=<?= htmlspecialchars($page) ?>">
|
||||
<label for="from_time">from</label>
|
||||
<input type="date" id="from_time" name="from_time"<?php if (isset($_REQUEST['from_time'])) echo " value=\"" . htmlspecialchars($from_time) . "\"" ?> />
|
||||
<label for="until_time">until</label>
|
||||
<input type="date" id="until_time" name="until_time"<?php if (isset($_REQUEST['until_time'])) echo " value=\"" . htmlspecialchars($until_time) . "\"" ?> />
|
||||
<input type="text" name="id" placeholder="component ID"<?php if (isset($_REQUEST['id'])) echo " value=\"" . htmlspecialchars($_REQUEST['id']) . "\"" ?> />
|
||||
<input type="text" name="name" placeholder="component name"<?php if (isset($_REQUEST['name'])) echo " value=\"" . htmlspecialchars($_REQUEST['name']) . "\"" ?> />
|
||||
<input type="text" name="event" placeholder="event name"<?php if (isset($_REQUEST['event'])) echo " value=\"" . htmlspecialchars($_REQUEST['event']) . "\"" ?> />
|
||||
<input type="button" onclick="clearFilter()" value="clear" />
|
||||
<input type="submit" value="search" />
|
||||
</form>
|
||||
<script>
|
||||
function clearFilter() {
|
||||
document.getElementById("filter_form").reset();
|
||||
const filterFields = document.querySelectorAll("#filter_form input");
|
||||
filterFields.forEach(input => {
|
||||
if (input.type === 'text' ||input.type === 'date') {
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
<!-- /Results filter -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- widget "<?= htmlspecialchars($widget['name']) ?>" -->
|
||||
<div class="collapse show" id="collapse<?= htmlspecialchars($widget['name']) ?>">
|
||||
<?php if ($time_range_specified) { ?>
|
||||
<p class="m-3">time period: <strong><?= htmlspecialchars($from_time) ?> - <?= htmlspecialchars($until_time) ?></strong></p>
|
||||
<?php } ?>
|
||||
<div class="mb-5">
|
||||
<?php if ($widget['full'] === true) { ?>
|
||||
<table class="table table-results table-striped table-hover table-bordered">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<?php foreach ($widget['table_headers'] as $header) { ?>
|
||||
<th scope="col"><?= htmlspecialchars($header) ?></th>
|
||||
<?php } ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($widget['table_records'] as $row) { ?>
|
||||
<tr>
|
||||
<?php foreach ($row as $key => $column) { ?>
|
||||
<?php if ($key === 'component ID') { ?>
|
||||
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&id=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
|
||||
<?php } elseif ($key === 'component') { ?>
|
||||
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&name=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
|
||||
<?php } else { ?>
|
||||
<td><?= htmlspecialchars($column ?? '') ?></td>
|
||||
<?php }
|
||||
} ?>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
if ($widget['pagination'] && $item_count > $items_per_page) {
|
||||
$url = "$app_root?platform=$platform_id&page=$page";
|
||||
include '../app/helpers/pagination.php';
|
||||
}
|
||||
?>
|
||||
<?php } else { ?>
|
||||
<p class="m-3">No matching records found.</p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "<?= htmlspecialchars($widget['name']) ?>" -->
|
|
@ -0,0 +1,94 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="card w-auto bg-light border-light card-body" style="flex-direction: row;"><?= $widget['title'] ?></div>
|
||||
|
||||
<!-- Results filter -->
|
||||
<div class="card w-auto bg-light border-light card-body text-right" style="text-align: right;">
|
||||
<form method="POST" id="filter_form" class="filter-results" action="?platform=<?= htmlspecialchars($platform_id) ?>&page=<?= htmlspecialchars($page) ?>">
|
||||
<label for="from_time">from</label>
|
||||
<input type="date" id="from_time" name="from_time"<?php if (isset($_REQUEST['from_time'])) echo " value=\"" . htmlspecialchars($from_time) . "\"" ?> />
|
||||
<label for="until_time">until</label>
|
||||
<input type="date" id="until_time" name="until_time"<?php if (isset($_REQUEST['until_time'])) echo " value=\"" . htmlspecialchars($until_time) . "\"" ?> />
|
||||
<input type="text" name="id" placeholder="conference ID"<?php if (isset($_REQUEST['id'])) echo " value=\"" . htmlspecialchars($_REQUEST['id']) . "\"" ?> />
|
||||
<input type="text" name="name" placeholder="conference name"<?php if (isset($_REQUEST['name'])) echo " value=\"" . htmlspecialchars($_REQUEST['name']) . "\"" ?> />
|
||||
<input type="button" onclick="clearFilter()" value="clear" />
|
||||
<input type="submit" value="search" />
|
||||
</form>
|
||||
<script>
|
||||
function clearFilter() {
|
||||
document.getElementById("filter_form").reset();
|
||||
const filterFields = document.querySelectorAll("#filter_form input");
|
||||
filterFields.forEach(input => {
|
||||
if (input.type === 'text' ||input.type === 'date') {
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
<!-- /Results filter -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- widget "<?= htmlspecialchars($widget['name']) ?>" -->
|
||||
<div class="collapse show" id="collapse<?= htmlspecialchars($widget['name']) ?>">
|
||||
<?php if ($time_range_specified) { ?>
|
||||
<p class="m-3">time period: <strong><?= htmlspecialchars($from_time) ?> - <?= htmlspecialchars($until_time) ?></strong></p>
|
||||
<?php } ?>
|
||||
<div class="mb-5">
|
||||
<?php if ($widget['full'] === true) { ?>
|
||||
<table class="table table-results table-striped table-hover table-bordered">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<?php foreach ($widget['table_headers'] as $header) { ?>
|
||||
<th scope="col"><?= htmlspecialchars($header) ?></th>
|
||||
<?php } ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($widget['table_records'] as $row) { ?>
|
||||
<tr>
|
||||
<?php $stats_id = false;
|
||||
$participant_ip = false;
|
||||
if (isset($row['event']) && $row['event'] === 'stats_id') $stats_id = true;
|
||||
if (isset($row['event']) && $row['event'] === 'pair selected') $participant_ip = true;
|
||||
|
||||
foreach ($row as $key => $column) {
|
||||
if ($key === 'conference ID' && isset($conferenceId) && $conferenceId === $column) { ?>
|
||||
<td><strong><?= htmlspecialchars($column ?? '') ?></strong></td>
|
||||
<?php } elseif ($key === 'conference ID') { ?>
|
||||
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=conferences&id=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
|
||||
<?php } elseif ($key === 'conference name' && isset($conferenceName) && $conferenceName === $column) { ?>
|
||||
<td><strong><?= htmlspecialchars($column ?? '') ?></strong></td>
|
||||
<?php } elseif ($key === 'conference name') { ?>
|
||||
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=conferences&name=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
|
||||
<?php } elseif ($key === 'participant ID') { ?>
|
||||
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=participants&id=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
|
||||
<?php } elseif ($stats_id && $key === 'parameter') { ?>
|
||||
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=participants&name=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
|
||||
<?php } elseif ($participant_ip && $key === 'parameter') { ?>
|
||||
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=participants&ip=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
|
||||
<?php
|
||||
// in general listings we don't show seconds and miliseconds
|
||||
} elseif ($key === 'start' || $key === 'end') { ?>
|
||||
<td><?= htmlspecialchars(substr($column ?? '', 0, -7)) ?></td>
|
||||
<?php } else { ?>
|
||||
<td><?= htmlspecialchars($column ?? '') ?></td>
|
||||
<?php }
|
||||
} ?>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
if ($widget['pagination'] && $item_count > $items_per_page) {
|
||||
$url = "$app_root?platform=$platform_id&page=$page";
|
||||
include '../app/helpers/pagination.php';
|
||||
}
|
||||
?>
|
||||
<?php } else { ?>
|
||||
<p class="m-3">No matching records found.</p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /widget "<?= htmlspecialchars($widget['name']) ?>" -->
|
|
@ -2,33 +2,19 @@
|
|||
<div class="card text-center w-50 mx-auto">
|
||||
<h2 class="card-header">Login</h2>
|
||||
<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 JILO!</strong><br />Please enter login credentials:</p>
|
||||
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login">
|
||||
<?php include CSRF_TOKEN_INCLUDE; ?>
|
||||
<div class="form-group mb-3">
|
||||
<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 _"
|
||||
required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
|
||||
pattern=".{8,}" title="Eight or more characters"
|
||||
required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<input type="text" name="username" placeholder="Username" required autofocus />
|
||||
<br />
|
||||
<input type="password" name="password" placeholder="Password" required />
|
||||
<br />
|
||||
<label for="remember_me">
|
||||
<input type="checkbox" id="remember_me" name="remember_me" />
|
||||
remember me
|
||||
</label>
|
||||
</div>
|
||||
<?php if (isset($_GET['redirect'])): ?>
|
||||
<input type="hidden" name="redirect" value="<?php echo htmlspecialchars($_GET['redirect']); ?>">
|
||||
<?php endif; ?>
|
||||
<br /> <br />
|
||||
<input type="submit" class="btn btn-primary" value="Login" />
|
||||
</form>
|
||||
<div class="mt-3">
|
||||
<a href="?page=login&action=forgot">forgot password?</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /login form -->
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue