Compare commits

...

130 Commits
v0.3 ... HEAD

Author SHA1 Message Date
Yasen Pramatarov aa530c20d2 Removes closing php tags 2025-03-17 12:38:24 +02:00
Yasen Pramatarov 69ce646bad Updates changelog 2025-03-03 21:44:35 +02:00
Yasen Pramatarov 08c20fa2b9 Updates changelog 2025-03-03 21:43:47 +02:00
Yasen Pramatarov e2daf22ad7 HTML fixes 2025-02-28 13:50:54 +02:00
Yasen Pramatarov 921f310ac1 Adds CSRF toek to registration 2025-02-24 22:05:20 +02:00
Yasen Pramatarov d9bee210d4 Adds 'ip' to validator 2025-02-24 14:47:23 +02:00
Yasen Pramatarov 2fc6940c11 Adds missing feedback messages to login and security 2025-02-24 14:08:05 +02:00
Yasen Pramatarov ecad8e2801 Adds back auto-blacklisting in rate limiter 2025-02-23 19:35:38 +02:00
Yasen Pramatarov 4a18c344c8 Fixes rate limiting bugs 2025-02-23 19:22:47 +02:00
Yasen Pramatarov 58633313e1 Fixes user tests 2025-02-23 18:03:19 +02:00
Yasen Pramatarov 0f6dda44b8 Gets the client IP from a central place 2025-02-23 17:58:26 +02:00
Yasen Pramatarov b4b5a7ac8f Fixes CSRF 2025-02-23 17:48:02 +02:00
Yasen Pramatarov a45e064c18 Fixes registration logging 2025-02-23 17:47:06 +02:00
Yasen Pramatarov ecb4e0fab4 Fixes login and register forms 2025-02-23 17:28:20 +02:00
Yasen Pramatarov 035681ab28 Fixes app root redirection on ratelimiting 2025-02-23 15:21:40 +02:00
Yasen Pramatarov 34779bb891 Adds proper logging to CSRF middleware 2025-02-23 13:51:36 +02:00
Yasen Pramatarov c61f42792f Adds logging to component class and switches to bound params 2025-02-23 13:15:46 +02:00
Yasen Pramatarov 788167e251 Switches settings to use the feedback messaging 2025-02-23 13:14:58 +02:00
Yasen Pramatarov 019f31cc05 Temporary fix for CSRF logging 2025-02-23 00:04:26 +02:00
Yasen Pramatarov 91aca75138 Fixes reload after new platform adding 2025-02-23 00:02:15 +02:00
Yasen Pramatarov 66fb6bf576 Adds CSRF to settings page 2025-02-23 00:01:59 +02:00
Yasen Pramatarov ad6ca25493 Adds CSRF tokens to settings edit page 2025-02-22 18:55:17 +02:00
Yasen Pramatarov 4b4cac7cec Fixes conferences page templates 2025-02-21 12:06:38 +02:00
Yasen Pramatarov 487c23da3e Fixes errors in ratelimiter 2025-02-21 11:44:52 +02:00
Yasen Pramatarov 4182ba6c1b Fixes errors in security page 2025-02-21 11:44:04 +02:00
Yasen Pramatarov 20094b5e42 Reserves test folders Functional and Utils 2025-02-20 10:46:35 +02:00
Yasen Pramatarov 9d5f87d86f Adds github test action 2025-02-20 10:43:23 +02:00
Yasen Pramatarov f0b487ca36 Reorganizes tests folder structure 2025-02-20 10:41:14 +02:00
Yasen Pramatarov 5327bde032 Adds tests for middleware 2025-02-19 15:31:01 +02:00
Yasen Pramatarov c2f63f6121 Adds security headers and CSRF protection tests 2025-02-19 11:08:42 +02:00
Yasen Pramatarov 9d0056f0a6 Adds transaction database methods (for the tests) 2025-02-18 16:46:56 +02:00
Yasen Pramatarov a399103305 Adds database execute and prepare (needed for the tests) 2025-02-18 16:45:25 +02:00
Yasen Pramatarov b7f8fce86e Replaces errors with exceptions in database class 2025-02-18 16:42:36 +02:00
Yasen Pramatarov c77b07b8a2 Removes the router test for now 2025-02-18 16:42:17 +02:00
Yasen Pramatarov 6fc3629014 Adds initial unit tests 2025-02-18 16:36:31 +02:00
Yasen Pramatarov 2da13af04c Bugfixes 2025-02-17 18:51:39 +02:00
Yasen Pramatarov 363fbf2a6b Reorganizes helper include files 2025-02-17 16:50:57 +02:00
Yasen Pramatarov 3953546ace Adds option to allow media on selected pages 2025-02-17 16:03:59 +02:00
Yasen Pramatarov b7e10363d0 Adds security headers include middleware 2025-02-17 15:52:46 +02:00
Yasen Pramatarov f53a3eef05 Fixes remnants of old messaging system 2025-02-17 15:47:36 +02:00
Yasen Pramatarov ae8d84012b Bugfixes 2025-02-17 15:41:35 +02:00
Yasen Pramatarov ddb86eae51 Bugfixes 2025-02-17 15:40:34 +02:00
Yasen Pramatarov 144dd6e742 Adds ratelimiting to some pages 2025-02-17 15:15:05 +02:00
Yasen Pramatarov c465fbfdf4 Pages ratelimit middleware 2025-02-17 15:05:44 +02:00
Yasen Pramatarov beafdf29fb Enhances ratelimiter to include page requests, configurable 2025-02-17 15:04:50 +02:00
Yasen Pramatarov 00e2a38087 Renames ratelimitTable to authRatelimitTable 2025-02-17 14:52:24 +02:00
Yasen Pramatarov 80bf3ee2ed Switches from session messages to feedback class ones 2025-02-17 14:46:19 +02:00
Yasen Pramatarov c32bbd518b Adds valifdation to profile page 2025-02-17 14:44:47 +02:00
Yasen Pramatarov 730a5c153e Adds session management 2025-02-17 14:36:00 +02:00
Yasen Pramatarov 3a9916e63b Renames messages to feedback 2025-02-17 10:24:50 +02:00
Yasen Pramatarov 3e9eb0d822 Renames messages to feedback 2025-02-16 10:18:26 +02:00
Yasen Pramatarov ef97dda39b Renames messages to feedback 2025-02-15 10:13:39 +02:00
Yasen Pramatarov 31f4a99d20 Removes hardcoded messages 2025-02-12 17:48:27 +02:00
Yasen Pramatarov 759059baad Adds new messages 2025-02-11 17:25:55 +02:00
Yasen Pramatarov cca0eb63a6 Adds validation to security pages 2025-02-10 19:33:24 +02:00
Yasen Pramatarov 6c37a082bf Adds validation to registration form 2025-02-10 19:25:17 +02:00
Yasen Pramatarov d2a9280d7d Omit sidebar for non-logged in users 2025-02-10 19:18:15 +02:00
Yasen Pramatarov 64d19f61f2 Bugfix, allows both "true" an "1" for registration enabled, 2025-02-06 13:18:19 +02:00
Yasen Pramatarov cadc7b7750 Adds validator class, to be used in forms validation 2025-02-06 13:14:29 +02:00
Yasen Pramatarov d84c015787 Fixes html 2025-01-30 18:55:09 +02:00
Yasen Pramatarov 27a4dca7c6 Adds CSRF checks to login/logout pages 2025-01-30 18:48:46 +02:00
Yasen Pramatarov 9c9a306f55 Implements security helper and CSRF middleware 2025-01-30 18:47:13 +02:00
Yasen Pramatarov be77376d85 Removes old bootstrap files 2025-01-30 13:47:29 +02:00
Yasen Pramatarov eecd74cc0f Foxes date and time on dashboard 2025-01-30 13:31:39 +02:00
Yasen Pramatarov b4df4b785a Fixes date and time display 2025-01-29 17:46:47 +02:00
Yasen Pramatarov 9b00e3d42c Rebuilds conferences page 2025-01-29 17:20:32 +02:00
Yasen Pramatarov 170e885251 Small final fixes to components and participants pages 2025-01-29 16:05:56 +02:00
Yasen Pramatarov a96b203021 HTML fixes 2025-01-29 15:56:08 +02:00
Yasen Pramatarov 057cc6dca5 Cleans up components page code 2025-01-29 15:55:45 +02:00
Yasen Pramatarov 11d4118e71 Rebuilds participants page 2025-01-29 15:47:10 +02:00
Yasen Pramatarov f13cad57d8 HTML fixes 2025-01-29 15:46:42 +02:00
Yasen Pramatarov b552a80203 Cleans up the old code 2025-01-29 10:46:18 +02:00
Yasen Pramatarov b971a76662 Rebuilds livejs pages 2025-01-29 10:46:06 +02:00
Yasen Pramatarov 25da7331f0 Adds more pages descriptions 2025-01-29 10:45:24 +02:00
Yasen Pramatarov 50b89f92ea Fixes HTML 2025-01-29 10:44:54 +02:00
Yasen Pramatarov 676e145349 Adds pages descrition 2025-01-29 10:43:37 +02:00
Yasen Pramatarov f952257c20 Cleans up old files 2025-01-28 21:19:47 +02:00
Yasen Pramatarov e6e91b19d0 Moves live configjs to separate page 2025-01-28 21:19:24 +02:00
Yasen Pramatarov 26c7660bfa Moves graphs to separate page 2025-01-28 21:18:48 +02:00
Yasen Pramatarov e50ac96b50 Moves latest data to separate page 2025-01-28 21:18:20 +02:00
Yasen Pramatarov 20a39f5c29 Reorganizes static files and libs 2025-01-28 21:16:34 +02:00
Yasen Pramatarov 6e4657e90f Fixes the html of agents pages 2025-01-28 15:54:42 +02:00
Yasen Pramatarov 779d3e0bf6 Updates latest data page functionality 2025-01-28 15:27:40 +02:00
Yasen Pramatarov a288d311c0 Troubleshoots platforms adding and deleting 2025-01-27 17:27:46 +02:00
Yasen Pramatarov f87c42a746 HTML fixes 2025-01-26 19:07:07 +02:00
Yasen Pramatarov 299327cf29 Fix config page title 2025-01-26 18:32:29 +02:00
Yasen Pramatarov eb512c4c1b Adds js messages to settings page 2025-01-26 18:18:43 +02:00
Yasen Pramatarov 7dfd50e19a Updates config page with the new js messages 2025-01-26 18:06:00 +02:00
Yasen Pramatarov dfdb24a550 Adds JS messages system in separate files for easy inclusion 2025-01-26 18:05:29 +02:00
Yasen Pramatarov e13bb7fc42 Moves our JS files in separate folder 2025-01-26 18:03:44 +02:00
Yasen Pramatarov 828020d689 Fixes config editing JS messages 2025-01-26 17:32:37 +02:00
Yasen Pramatarov 4a8185839d Fixes HTML of config page 2025-01-26 15:33:45 +02:00
Yasen Pramatarov 71d0984e9d Redesigns the components page 2025-01-26 14:39:10 +02:00
Yasen Pramatarov 4e79b76377 HTML fix 2025-01-26 14:07:58 +02:00
Yasen Pramatarov fc16bea465 HTML fixes 2025-01-26 14:07:28 +02:00
Yasen Pramatarov df200aae64 Redesignes and fixes the agents page 2025-01-26 00:11:19 +02:00
Yasen Pramatarov 06cc20fb2a Fixes bug in configjs and interfaceconfigjs pages 2025-01-24 16:13:45 +02:00
Yasen Pramatarov 5a451115f4 Highlights selected host or agent 2025-01-24 16:03:26 +02:00
Yasen Pramatarov fc71cdd7f8 Fixes status page design 2025-01-24 11:48:37 +02:00
Yasen Pramatarov e59920cfd0 Troubleshoots settings page 2025-01-23 18:40:55 +02:00
Yasen Pramatarov 6e6f4f6694 Desing fixes 2025-01-23 14:16:00 +02:00
Yasen Pramatarov 752f519ccc Enhances messages system with JS-based messages 2025-01-23 14:06:36 +02:00
Yasen Pramatarov ffe08f913b Updates design of config page, adds ajax 2025-01-23 12:42:27 +02:00
Yasen Pramatarov 1f75f81297 Moves Jitsi platforms configs to settings 2025-01-23 12:41:29 +02:00
Yasen Pramatarov b9e85c65bd Updates user rights 2025-01-23 12:40:11 +02:00
Yasen Pramatarov e3b8cccba3 Ad javascript option to messaging 2025-01-23 12:37:10 +02:00
Yasen Pramatarov 5f9702848e Redesigns the help page 2025-01-23 09:53:08 +02:00
Yasen Pramatarov 5dc419b7a7 Fixes deletion of platforms with hosts/agents in them 2025-01-23 00:26:40 +02:00
Yasen Pramatarov aa2dcc027d Makes required fields for hosts and agents 2025-01-23 00:17:09 +02:00
Yasen Pramatarov f0b98d3063 Simplifies template for logs page 2025-01-23 00:15:52 +02:00
Yasen Pramatarov 5b24d098e4 Debugs config items management 2025-01-22 22:52:50 +02:00
Yasen Pramatarov 53b3965a32 Improves design of config items delete 2025-01-22 18:45:23 +02:00
Yasen Pramatarov d0fa120202 Fixes SQL for platform->host->agent 2025-01-22 17:49:15 +02:00
Yasen Pramatarov fc1ed97499 Fixes platform->host->agent logic 2025-01-22 17:46:08 +02:00
Yasen Pramatarov e3f839bc56 Puts all jilo config in one page with ajax 2025-01-21 16:57:28 +02:00
Yasen Pramatarov d45ba62805 Moves agent editing to in-place ajax forms 2025-01-20 21:42:22 +02:00
Yasen Pramatarov 5321942da8 Moved platform and host editing to in-place ajax 2025-01-20 21:19:33 +02:00
Yasen Pramatarov 405f58124d Redesigns the whole config page 2025-01-20 21:17:54 +02:00
Yasen Pramatarov 1e4ebae652 Fixes cancel button 2025-01-20 21:17:28 +02:00
Yasen Pramatarov e932e4899c Alignes page title 2025-01-20 21:16:26 +02:00
Yasen Pramatarov 7c8335d3e7 Removes unused old config templates 2025-01-20 21:15:29 +02:00
Yasen Pramatarov 81287a2c95 Removes port from host config 2025-01-20 21:14:43 +02:00
Yasen Pramatarov 9c3964da20 Adds agents management pages 2025-01-19 17:46:50 +02:00
Yasen Pramatarov 3c9cce2c8b Fixes sidebar menu entries 2025-01-18 19:41:28 +02:00
Yasen Pramatarov 35020a0108 Fixes logs search 2025-01-18 17:41:03 +02:00
Yasen Pramatarov e85292b58f Adds proper pagination. Paginates logs page. 2025-01-18 17:24:30 +02:00
Yasen Pramatarov 9fd2af6538 Updates design and fixes the logs page 2025-01-18 13:17:32 +02:00
Yasen Pramatarov 949ce27f63 HTML fixes 2025-01-17 18:29:41 +02:00
Yasen Pramatarov 81b66db3c6 Updates changelog 2025-01-17 17:59:58 +02:00
Yasen Pramatarov b2fcaf6793 HTML fixes 2025-01-17 16:08:37 +02:00
149 changed files with 8566 additions and 2790 deletions

30
.github/workflows/tests.yml vendored 100644
View File

@ -0,0 +1,30 @@
name: PHP Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: pdo, pdo_sqlite
- name: Install dependencies
run: |
cd tests
composer install
- name: Run test suite
run: |
cd tests
./vendor/bin/phpunit

View File

@ -4,6 +4,40 @@ All notable changes to this project will be documented in this file.
---
## Unreleased
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.3...HEAD
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.3...HEAD
- github: https://github.com/lindeas/jilo-web/compare/v0.3...HEAD
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.3...HEAD
### Added
- 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
- 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
- Fixed logs search
- Removed hardcoded messages, changed to feedback messages
---
## 0.3 - 2025-01-15
#### Links

View File

@ -24,29 +24,32 @@ class Agent {
/**
* Retrieves details of a specified agent ID (or all agents) in a specified platform.
* Retrieves details of agents for a specified host.
*
* @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.
* @param int $host_id The host ID to filter agents by.
* @param int $agent_id Optional agent ID to filter by.
*
* @return array The list of agent details.
*/
public function getAgentDetails($platform_id, $agent_id = '') {
public function getAgentDetails($host_id, $agent_id = '') {
$sql = 'SELECT
ja.id,
ja.platform_id,
ja.host_id,
ja.agent_type_id,
ja.url,
ja.secret_key,
ja.check_period,
jat.description AS agent_description,
jat.endpoint AS agent_endpoint
jat.endpoint AS agent_endpoint,
h.platform_id
FROM
jilo_agents ja
JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN
hosts h ON ja.host_id = h.id
WHERE
platform_id = :platform_id';
ja.host_id = :host_id';
if ($agent_id !== '') {
$sql .= ' AND ja.id = :agent_id';
@ -54,7 +57,7 @@ class Agent {
$query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id);
$query->bindParam(':host_id', $host_id);
if ($agent_id !== '') {
$query->bindParam(':agent_id', $agent_id);
}
@ -75,17 +78,20 @@ class Agent {
public function getAgentIDDetails($agent_id) {
$sql = 'SELECT
ja.id,
ja.platform_id,
ja.host_id,
ja.agent_type_id,
ja.url,
ja.secret_key,
ja.check_period,
jat.description AS agent_description,
jat.endpoint AS agent_endpoint
jat.endpoint AS agent_endpoint,
h.platform_id
FROM
jilo_agents ja
JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN
hosts h ON ja.host_id = h.id
WHERE
ja.id = :agent_id';
@ -114,22 +120,22 @@ class Agent {
/**
* Retrieves agent types already configured for a specific platform.
* Retrieves agent types already configured for a specific host.
*
* @param int $platform_id The platform ID to filter agents by.
* @param int $host_id The host ID to filter agents by.
*
* @return array List of agent types configured for the platform.
* @return array List of agent types configured for the host.
*/
public function getPlatformAgentTypes($platform_id) {
public function getHostAgentTypes($host_id) {
$sql = 'SELECT
id,
agent_type_id
FROM
jilo_agents
WHERE
platform_id = :platform_id';
host_id = :host_id';
$query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id);
$query->bindParam(':host_id', $host_id);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
@ -137,27 +143,27 @@ class Agent {
/**
* Adds a new agent to the platform.
* Add a new agent to the database.
*
* @param int $platform_id The platform ID where the agent is to be added.
* @param array $newAgent The new agent details to add.
* @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.
*
* @return bool|string Returns true on success or an error message on failure.
* @return bool|string True if the agent was added successfully, otherwise error message.
*/
public function addAgent($platform_id, $newAgent) {
public function addAgent($host_id, $newAgent) {
try {
$sql = 'INSERT INTO jilo_agents
(platform_id, agent_type_id, url, secret_key, check_period)
(host_id, agent_type_id, url, secret_key, check_period)
VALUES
(:platform_id, :agent_type_id, :url, :secret_key, :check_period)';
(:host_id, :agent_type_id, :url, :secret_key, :check_period)';
$query = $this->db->prepare($sql);
$query->execute([
':platform_id' => $platform_id,
':agent_type_id' => $newAgent['type_id'],
':url' => $newAgent['url'],
':secret_key' => $newAgent['secret_key'],
':check_period' => $newAgent['check_period'],
':host_id' => $host_id,
':agent_type_id' => $newAgent['type_id'],
':url' => $newAgent['url'],
':secret_key' => $newAgent['secret_key'],
':check_period' => $newAgent['check_period'],
]);
return true;
@ -169,33 +175,34 @@ class Agent {
/**
* Edits an existing agent's details.
* Edit an existing agent in the database.
*
* @param int $platform_id The platform ID where the agent exists.
* @param array $updatedAgent The updated agent 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.
*
* @return bool|string Returns true on success or an error message on failure.
* @return bool|string True if the agent was updated successfully, otherwise error message.
*/
public function editAgent($platform_id, $updatedAgent) {
public function editAgent($agent_id, $updatedAgent) {
try {
$sql = 'UPDATE jilo_agents 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
AND
platform_id = :platform_id';
id = :agent_id';
// Convert empty secret key to NULL
$secretKey = !empty($updatedAgent['secret_key']) ? $updatedAgent['secret_key'] : null;
$query = $this->db->prepare($sql);
$query->execute([
':agent_type_id' => $updatedAgent['agent_type_id'],
':url' => $updatedAgent['url'],
':secret_key' => $updatedAgent['secret_key'],
':check_period' => $updatedAgent['check_period'],
':agent_id' => $updatedAgent['id'],
':platform_id' => $platform_id,
':agent_id' => $agent_id,
':agent_type_id' => $updatedAgent['agent_type_id'],
':url' => $updatedAgent['url'],
':secret_key' => $secretKey,
':check_period' => $updatedAgent['check_period'],
]);
return true;
@ -207,7 +214,7 @@ class Agent {
/**
* Deletes an agent from the platform.
* Deletes an agent from the database.
*
* @param int $agent_id The agent ID to delete.
*
@ -279,7 +286,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
@ -398,15 +405,15 @@ class Agent {
}
/**
* Retrieves the latest stored data for a specific platform, agent type, and metric type.
* Retrieves the latest stored data for a specific host, agent type, and metric type.
*
* @param int $platform_id The platform ID.
* @param int $host_id The host 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($platform_id, $agent_type, $metric_type) {
public function getLatestData($host_id, $agent_type, $metric_type) {
$sql = 'SELECT
jac.timestamp,
jac.response_content,
@ -418,8 +425,10 @@ class Agent {
jilo_agents ja ON jac.agent_id = ja.id
JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN
hosts h ON ja.host_id = h.id
WHERE
ja.platform_id = :platform_id
h.id = :host_id
AND jat.description = :agent_type
AND jac.status_code = 200
ORDER BY
@ -428,7 +437,7 @@ class Agent {
$query = $this->db->prepare($sql);
$query->execute([
':platform_id' => $platform_id,
':host_id' => $host_id,
':agent_type' => $agent_type
]);
@ -497,14 +506,14 @@ class Agent {
/**
* Gets historical data for a specific metric from agent checks
*
* @param int $platform_id The platform ID
* @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 $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($platform_id, $agent_type, $metric_type, $from_time, $until_time) {
public function getHistoricalData($host_id, $agent_type, $metric_type, $from_time, $until_time) {
// Get data from agent checks
$sql = 'SELECT
DATE(jac.timestamp) as date,
@ -516,8 +525,10 @@ class Agent {
jilo_agents ja ON jac.agent_id = ja.id
JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN
hosts h ON ja.host_id = h.id
WHERE
ja.platform_id = :platform_id
h.id = :host_id
AND jat.description = :agent_type
AND jac.status_code = 200
AND DATE(jac.timestamp) BETWEEN :from_time AND :until_time
@ -528,7 +539,7 @@ class Agent {
$query = $this->db->prepare($sql);
$query->execute([
':platform_id' => $platform_id,
':host_id' => $host_id,
':agent_type' => $agent_type,
':from_time' => $from_time,
':until_time' => $until_time
@ -565,6 +576,72 @@ 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_checks jac
JOIN
jilo_agents ja ON jac.agent_id = ja.id
JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id
JOIN
hosts 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;
}
}

View File

@ -36,59 +36,134 @@ 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';
}
if (strlen($until_time) <= 10) {
$until_time .= ' 23:59:59';
}
// time period drill-down
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
// 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)";
}
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";
if ($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, "'") . '%');
}
$stmt->bindParam(':from_time', $from_time);
$stmt->bindParam(':until_time', $until_time);
if ($items_per_page) {
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->bindParam(':items_per_page', $items_per_page, PDO::PARAM_INT);
}
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($result)) {
$logObject->insertLog(0, "Retrieved " . count($result) . " Jitsi component events");
}
return $result;
} catch (PDOException $e) {
$logObject->insertLog(0, "Failed to retrieve Jitsi component events: " . $e->getMessage());
return [];
}
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
jitsi_component = %s
AND
component_id = %s";
if ($event_type != '' && $event_type != 'event_type') {
$sql .= "
AND
event_type LIKE '%%%s%%'";
}
$sql .= "
AND
(time >= '%s 00:00:00' AND time <= '%s 23:59:59')
ORDER BY
time";
if ($items_per_page) {
$items_per_page = (int)$items_per_page;
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
}
// 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);
}
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
}
/**
* 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->insertLog(0, "Failed to retrieve component events count: " . $e->getMessage());
return 0;
}
}
}

View File

@ -382,5 +382,3 @@ SELECT COUNT(*) AS conferences
}
?>

View File

@ -3,15 +3,15 @@
/**
* class Config
*
* Handles editing and fetching configuration files.
* Handles editing and fetching ot the config files.
*/
class Config {
/**
* Edits a configuration file by updating specified options.
* Edits a config file by updating specified options.
*
* @param array $updatedConfig Key-value pairs of configuration options to update.
* @param string $config_file Path to the configuration file.
* @param array $updatedConfig Key-value pairs of config options to update.
* @param string $config_file Path to the config file.
*
* @return mixed Returns true on success, or an error message on failure.
*/
@ -50,104 +50,4 @@ class Config {
return true;
}
/**
* 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';
// default content, if we can't get the file contents
$platformConfigjs = "The file $configjsFile 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($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;
}
}
?>

View File

@ -28,13 +28,13 @@ class Database {
*/
public function __construct($options) {
// check if PDO extension is loaded
if ( !extension_loaded('pdo') ) {
$error = getError('PDO extension not loaded.');
if (!extension_loaded('pdo')) {
throw new Exception('PDO extension not loaded.');
}
// options check
if (empty($options['type'])) {
$error = getError('Database type is not set.');
throw new Exception('Database type is not set.');
}
// connect based on database type
@ -42,15 +42,15 @@ class Database {
case 'sqlite':
$this->connectSqlite($options);
break;
case 'mysql' || 'mariadb':
case 'mysql':
case 'mariadb':
$this->connectMysql($options);
break;
default:
$error = getError("Database type \"{$options['type']}\" is not supported.");
$this->pdo = null;
}
}
/**
* Establishes a connection to a SQLite database.
*
@ -62,12 +62,17 @@ class Database {
private function connectSqlite($options) {
// pdo_sqlite extension is needed
if (!extension_loaded('pdo_sqlite')) {
$error = getError('PDO extension for SQLite not loaded.');
throw new Exception('PDO extension for SQLite not loaded.');
}
// SQLite options
if (empty($options['dbFile']) || !file_exists($options['dbFile'])) {
$error = getError("SQLite database file \"{$options['dbFile']}\" not found.");
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.");
}
// connect to SQLite
@ -77,11 +82,10 @@ class Database {
// enable foreign key constraints (not ON by default in SQLite3)
$this->pdo->exec('PRAGMA foreign_keys = ON;');
} catch (PDOException $e) {
$error = getError('SQLite connection failed: ', $e->getMessage());
throw new Exception('SQLite connection failed: ' . $e->getMessage());
}
}
/**
* Establishes a connection to a MySQL (or MariaDB) database.
*
@ -97,25 +101,25 @@ class Database {
private function connectMysql($options) {
// pdo_mysql extension is needed
if (!extension_loaded('pdo_mysql')) {
$error = getError('PDO extension for MySQL not loaded.');
throw new Exception('PDO extension for MySQL not loaded.');
}
// MySQL options
if (empty($options['host']) || empty($options['dbname']) || empty($options['user'])) {
$error = getError('MySQL connection data is missing.');
throw new Exception('MySQL connection data is missing.');
}
// Connect to MySQL
try {
$dsn = "mysql:host={$options['host']};port={$options['port']};dbname={$options['dbname']};charset=utf8";
$port = $options['port'] ?? 3306;
$dsn = "mysql:host={$options['host']};port={$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) {
$error = getError('MySQL connection failed: ', $e->getMessage());
$this->pdo = null;
}
}
/**
* Retrieves the current PDO connection instance.
*
@ -125,6 +129,95 @@ 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());
}
}
}

View File

@ -1,13 +1,13 @@
<?php
class Messages {
// Message types
class Feedback {
// Feedback types
const TYPE_SUCCESS = 'success';
const TYPE_ERROR = 'danger';
const TYPE_INFO = 'info';
const TYPE_WARNING = 'warning';
// Default message configurations
// Default feedback message configurations
const NOTICE = [
'DEFAULT' => [
'type' => self::TYPE_INFO,
@ -96,20 +96,50 @@ class Messages {
]
];
const REGISTER = [
'SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'FAILED' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'DISABLED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
];
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 message strings
* Get feedback message strings
*/
private static function getStrings() {
if (self::$strings === null) {
self::$strings = require __DIR__ . '/../includes/messages-strings.php';
self::$strings = require __DIR__ . '/../includes/strings.php';
}
return self::$strings;
}
/**
* Get message configuration by key
* Get feedback message configuration by key
*/
public static function get($category, $key) {
$config = constant("self::$category")[$key] ?? null;
@ -122,9 +152,9 @@ class Messages {
}
/**
* Render message HTML
* Render feedback message HTML
*/
// Usage: echo Messages::render('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
// Usage: echo Feedback::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 '';
@ -146,15 +176,30 @@ class Messages {
}
/**
* Store message in session for display after redirect
* Get feedback message data for JavaScript
*/
// Usage: Messages::flash('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
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]);
public static function flash($category, $key, $customMessage = null, $dismissible = null, $small = false) {
if (!isset($_SESSION['flash_messages'])) {
$_SESSION['flash_messages'] = [];
}
// Get the message configuration
// Get the feedback message configuration
$config = self::get($category, $key);
$isDismissible = $dismissible ?? $config['dismissible'] ?? false;
@ -168,11 +213,11 @@ class Messages {
}
/**
* Get and clear all flash messages
* Get and clear all flash feedback messages
*/
public static function getFlash() {
$messages = $_SESSION['flash_messages'] ?? [];
$system_messages = $_SESSION['flash_messages'] ?? [];
unset($_SESSION['flash_messages']);
return $messages;
return $system_messages;
}
}

View File

@ -34,7 +34,6 @@ class Host {
$sql = 'SELECT
id,
address,
port,
platform_id,
name
FROM
@ -73,14 +72,13 @@ class Host {
public function addHost($newHost) {
try {
$sql = 'INSERT INTO hosts
(address, port, platform_id, name)
(address, platform_id, name)
VALUES
(:address, :port, :platform_id, :name)';
(:address, :platform_id, :name)';
$query = $this->db->prepare($sql);
$query->execute([
':address' => $newHost['address'],
':port' => $newHost['port'],
':platform_id' => $newHost['platform_id'],
':name' => $newHost['name'],
]);
@ -99,25 +97,28 @@ 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 True if the host was updated successfully, otherwise false.
* @return bool|string True if the host was updated successfully, otherwise error message.
*/
public function editHost($platform_id, $updatedHost) {
try {
$sql = 'UPDATE hosts SET
address = :address,
port = :port,
name = :name
WHERE
id = :id';
id = :id AND platform_id = :platform_id';
$query = $this->db->prepare($sql);
$query->execute([
':id' => $updatedHost['id'],
':address' => $updatedHost['address'],
':port' => $updatedHost['port'],
':name' => $updatedHost['name'],
':id' => $updatedHost['id'],
':platform_id' => $platform_id,
':address' => $updatedHost['address'],
':name' => $updatedHost['name']
]);
if ($query->rowCount() === 0) {
return "No host found with ID {$updatedHost['id']} in platform $platform_id";
}
return true;
} catch (Exception $e) {
@ -135,21 +136,30 @@ class Host {
*/
public function deleteHost($host_id) {
try {
$sql = 'DELETE FROM hosts
WHERE
id = :host_id';
// Start transaction
$this->db->beginTransaction();
// First delete all agents associated with this host
$sql = 'DELETE FROM jilo_agents 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 hosts 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();
}
}
}
?>

View File

@ -21,7 +21,6 @@ class Log {
$this->db = $database->getConnection();
}
/**
* Insert a log event into the database.
*
@ -40,9 +39,9 @@ class Log {
$query = $this->db->prepare($sql);
$query->execute([
':user_id' => $user_id,
':scope' => $scope,
':message' => $message,
':user_id' => $user_id,
':scope' => $scope,
':message' => $message,
]);
return true;
@ -52,7 +51,6 @@ class Log {
}
}
/**
* Retrieve log entries from the database.
*
@ -60,36 +58,65 @@ class Log {
* @param string $scope The scope of the logs ('user' or 'system').
* @param int $offset The offset for pagination. Default is 0.
* @param int $items_per_page The number of log entries to retrieve per page. Default is no limit.
* @param array $filters Optional array of filters (from_time, until_time, message, id)
*
* @return array An array of log entries.
*/
public function readLog($user_id, $scope, $offset=0, $items_per_page='') {
public function readLog($user_id, $scope, $offset=0, $items_per_page='', $filters=[]) {
$params = [];
$where_clauses = [];
// Base query with user join
$base_sql = 'SELECT l.*, u.username
FROM logs l
LEFT JOIN users u ON l.user_id = u.id';
// Add scope condition
if ($scope === 'user') {
$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,
]);
$where_clauses[] = 'l.user_id = :user_id';
$params[':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();
// Add time range filters if specified
if (!empty($filters['from_time'])) {
$where_clauses[] = 'l.time >= :from_time';
$params[':from_time'] = $filters['from_time'] . ' 00:00:00';
}
if (!empty($filters['until_time'])) {
$where_clauses[] = 'l.time <= :until_time';
$params[':until_time'] = $filters['until_time'] . ' 23:59:59';
}
// Add message search if specified
if (!empty($filters['message'])) {
$where_clauses[] = 'l.message LIKE :message';
$params[':message'] = '%' . $filters['message'] . '%';
}
// Add user ID search if specified
if (!empty($filters['id'])) {
$where_clauses[] = 'l.user_id = :search_user_id';
$params[':search_user_id'] = $filters['id'];
}
// Combine WHERE clauses
$sql = $base_sql;
if (!empty($where_clauses)) {
$sql .= ' WHERE ' . implode(' AND ', $where_clauses);
}
// Add ordering
$sql .= ' ORDER BY l.time DESC';
// Add pagination
if ($items_per_page) {
$items_per_page = (int)$items_per_page;
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
}
$query = $this->db->prepare($sql);
$query->execute($params);
return $query->fetchAll(PDO::FETCH_ASSOC);
}
}
?>

View File

@ -372,5 +372,3 @@ AND pe.event_type = 'participant joining'";
}
}
?>

View File

@ -122,21 +122,42 @@ class Platform {
*/
public function deletePlatform($platform_id) {
try {
$sql = 'DELETE FROM platforms
WHERE
id = :platform_id';
$this->db->beginTransaction();
// First, get all hosts in this platform
$sql = 'SELECT id FROM hosts 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_agents 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 hosts 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 platforms 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();
}
}
}
?>

View File

@ -7,9 +7,20 @@ class RateLimiter {
public $decayMinutes = 15; // Time window in minutes
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
public $autoBlacklistDuration = 24; // Hours to blacklist for
public $ratelimitTable = 'login_attempts';
public $authRatelimitTable = 'login_attempts'; // For username/password attempts
public $pagesRatelimitTable = 'pages_rate_limits'; // For page requests
public $whitelistTable = 'ip_whitelist';
public $blacklistTable = 'ip_blacklist';
private $pageLimits = [
// Default rate limits per minute
'default' => 60,
'admin' => 120,
'message' => 20,
'contact' => 30,
'call' => 30,
'register' => 5,
'config' => 10
];
public function __construct($database) {
$this->db = $database->getConnection();
@ -19,15 +30,24 @@ class RateLimiter {
// Database preparation
private function createTablesIfNotExist() {
// Login attempts table
$sql = "CREATE TABLE IF NOT EXISTS {$this->ratelimitTable} (
// Authentication attempts table
$sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
ip_address TEXT NOT NULL,
username TEXT NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now'))
)";
$this->db->exec($sql);
// Pages rate limits table
$sql = "CREATE TABLE IF NOT EXISTS {$this->pagesRatelimitTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
endpoint TEXT NOT NULL,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP
)";
$this->db->exec($sql);
// IP whitelist table
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -91,7 +111,7 @@ 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->ratelimitTable}
$stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->authRatelimitTable}
WHERE ip_address = ? AND attempted_at > datetime('now', '-' || :minutes || ' minutes')");
$stmt->execute([$ip, $this->decayMinutes]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
@ -103,7 +123,20 @@ 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}");
$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->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
@ -112,14 +145,8 @@ class RateLimiter {
continue;
}
if ($row['is_network']) {
if ($this->ipInRange($ip, $row['ip_address'])) {
return true;
}
} else {
if ($ip === $row['ip_address']) {
return true;
}
if ($this->ipInRange($ip, $row['ip_address'])) {
return true;
}
}
@ -130,19 +157,24 @@ class RateLimiter {
* Check if an IP is whitelisted
*/
public function isIpWhitelisted($ip) {
// Check exact IP match and CIDR ranges
$stmt = $this->db->prepare("SELECT ip_address, is_network FROM {$this->whitelistTable}");
$stmt->execute();
// 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;
}
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
if ($row['is_network']) {
// 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");
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
if ($this->ipInRange($ip, $row['ip_address'])) {
return true;
}
} else {
if ($ip === $row['ip_address']) {
return true;
}
}
}
@ -150,8 +182,18 @@ 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);
@ -168,7 +210,7 @@ class RateLimiter {
$message = "Cannot whitelist {$ip} - IP is currently blacklisted";
if ($userId) {
$this->log->insertLog($userId, "IP Whitelist: {$message}", 'system');
Messages::flash('ERROR', 'DEFAULT', $message);
Feedback::flash('ERROR', 'DEFAULT', $message);
}
return false;
}
@ -195,7 +237,7 @@ class RateLimiter {
} catch (Exception $e) {
if ($userId) {
$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());
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage());
}
return false;
}
@ -230,7 +272,7 @@ class RateLimiter {
} catch (Exception $e) {
if ($userId) {
$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());
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage());
}
return false;
}
@ -243,7 +285,7 @@ class RateLimiter {
$message = "Cannot blacklist {$ip} - IP is currently whitelisted";
if ($userId) {
$this->log->insertLog($userId, "IP Blacklist: {$message}", 'system');
Messages::flash('ERROR', 'DEFAULT', $message);
Feedback::flash('ERROR', 'DEFAULT', $message);
}
return false;
}
@ -272,7 +314,7 @@ class RateLimiter {
} catch (Exception $e) {
if ($userId) {
$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());
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage());
}
return false;
}
@ -287,6 +329,7 @@ class RateLimiter {
// Remove the IP
$stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable} WHERE ip_address = ?");
$result = $stmt->execute([$ip]);
if ($result && $ipDetails) {
@ -305,7 +348,7 @@ class RateLimiter {
} catch (Exception $e) {
if ($userId) {
$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());
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage());
}
return false;
}
@ -333,14 +376,14 @@ class RateLimiter {
$stmt->execute();
// Clean old login attempts
$stmt = $this->db->prepare("DELETE FROM {$this->ratelimitTable}
$stmt = $this->db->prepare("DELETE FROM {$this->authRatelimitTable}
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')");
$stmt->execute([':minutes' => $this->decayMinutes]);
return true;
} catch (Exception $e) {
$this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system');
Messages::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage());
Feedback::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage());
return false;
}
}
@ -366,7 +409,7 @@ class RateLimiter {
// Check total attempts across all usernames from this IP
$sql = "SELECT COUNT(*) as total_attempts
FROM {$this->ratelimitTable}
FROM {$this->authRatelimitTable}
WHERE ip_address = :ip
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql);
@ -382,23 +425,14 @@ class RateLimiter {
public function attempt($username, $ipAddress) {
// Record this attempt
$sql = "INSERT INTO {$this->ratelimitTable} (ip_address, username) VALUES (:ip, :username)";
$sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':username' => $username
]);
// 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
);
try {
$stmt->execute([
':ip' => $ipAddress,
':username' => $username
]);
} catch (PDOException $e) {
return false;
}
@ -407,7 +441,7 @@ class RateLimiter {
public function tooManyAttempts($username, $ipAddress) {
$sql = "SELECT COUNT(*) as attempts
FROM {$this->ratelimitTable}
FROM {$this->authRatelimitTable}
WHERE ip_address = :ip
AND username = :username
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
@ -420,11 +454,32 @@ class RateLimiter {
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result['attempts'] >= $this->maxAttempts;
// 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;
}
public function clearOldAttempts() {
$sql = "DELETE FROM {$this->ratelimitTable}
$sql = "DELETE FROM {$this->authRatelimitTable}
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql);
@ -435,7 +490,7 @@ class RateLimiter {
public function getRemainingAttempts($username, $ipAddress) {
$sql = "SELECT COUNT(*) as attempts
FROM {$this->ratelimitTable}
FROM {$this->authRatelimitTable}
WHERE ip_address = :ip
AND username = :username
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
@ -454,4 +509,121 @@ 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 >= DATETIME('now', '-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 < DATETIME('now', '-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) {
$userObj = new User($this->db);
if ($userObj->hasRight($userId, 'admin')) {
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 > DATETIME('now', '-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']);
}
}

View File

@ -83,5 +83,3 @@ class Router {
}
}
?>

View File

@ -52,5 +52,3 @@ class Server {
}
}
?>

View File

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

View File

@ -90,8 +90,9 @@ class User {
* @return bool True if login is successful, false otherwise.
*/
public function login($username, $password) {
// get client IP address
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
// Get user's IP address
require_once __DIR__ . '/../helpers/logs.php';
$ipAddress = getUserIP();
// Record attempt
$this->rateLimiter->attempt($username, $ipAddress);
@ -450,5 +451,3 @@ class User {
}
}
?>

View File

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

View File

@ -11,7 +11,7 @@ return [
// subfolder for the web app, if any
'folder' => '/jilo-web/',
// set to false to disable new registrations
'registration_enabled' => true,
'registration_enabled' => '1',
// will be displayed on login screen
'login_message' => '',
@ -36,5 +36,3 @@ return [
'environment' => 'development',
];
?>

View File

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

View File

@ -18,5 +18,3 @@ function getUserIP() {
return trim($ip);
}
?>

View File

@ -44,5 +44,3 @@ function switchPlatform($platform_id) {
// return the new URL with the new platform_id
return $new_url;
}
?>

View File

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

View File

@ -11,5 +11,3 @@ if (!isset($until_time) || (isset($until_time) && $until_time == '')) {
} else {
$time_range_specified = true;
}
?>

View File

@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/../helpers/security.php';
require_once __DIR__ . '/../helpers/logs.php';
function applyCsrfMiddleware() {
$security = SecurityHelper::getInstance();
// Skip CSRF check for GET requests
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
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') {
$token = $_POST['csrf_token'] ?? '';
if (!$security->verifyCsrfToken($token)) {
// Log CSRF attempt
$ipAddress = getUserIP();
$logMessage = sprintf(
"CSRF attempt detected - IP: %s, Page: %s, User: %s",
$ipAddress,
$_GET['page'] ?? 'unknown',
$_SESSION['username'] ?? 'anonymous'
);
$logObject->insertLog(0, $logMessage, 'system');
// Return error message
http_response_code(403);
die('Invalid CSRF token. Please try again.');
}
}
return true;
}

View File

@ -52,11 +52,10 @@ function connectDB($config, $database = '', $dbFile = '', $platformId = '') {
// unknown database
} else {
$error = "Error: unknow database type \"{$config['db']['db_type']}\"";
Messages::flash('ERROR', 'DEFAULT', $error);
Feedback::flash('ERROR', 'DEFAULT', $error);
exit();
}
}
}
?>

View File

@ -38,5 +38,3 @@ function renderMessage(&$message, $type, $unset = false) {
}
}
}
?>

View File

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

View File

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

View File

@ -0,0 +1,59 @@
<?php
require_once __DIR__ . '/../classes/ratelimiter.php';
require_once __DIR__ . '/../helpers/logs.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;
$isTest = defined('PHPUNIT_RUNNING');
$rateLimiter = $existingRateLimiter ?? new RateLimiter($database);
$ipAddress = getUserIP();
// 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;
}

View File

@ -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,6 +59,3 @@ if (isset($_POST['check_period'])) {
if (isset($_POST['name'])) {
$name = htmlspecialchars($_POST['name']);
}
?>

View File

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

View File

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

View File

@ -2,6 +2,11 @@
// 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.',
@ -12,13 +17,15 @@ return [
],
'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_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_ERROR' => 'Failed to remove IP from whitelist.',
'WHITELIST_REMOVE_FAILED' => '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_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_ERROR' => 'Failed to remove IP 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.',

View File

@ -8,61 +8,167 @@
* to allow time-based invalidation if needed.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// 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);
$action = $_REQUEST['action'] ?? '';
$agent = $_REQUEST['agent'] ?? '';
require '../app/classes/agent.php';
require '../app/classes/host.php';
$agentObject = new Agent($dbWeb);
$hostObject = new Host($dbWeb);
// if it's a POST request, it's 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);
// 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 {$agent} 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 {$agent} is cleared."
]);
} else {
echo json_encode([
'status' => 'error',
'message' => 'Invalid data'
]);
}
//// 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']);
// }
// no form submitted, show the templates
} else {
$agentDetails = $agentObject->getAgentDetails($platform_id);
include '../app/templates/agent-list.php';
/**
* 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}";
}
?>
/**
* 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;
}
// Handle POST request (saving to cache)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Apply rate limiting for adding new contacts
require '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'contact', $user_id);
// 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.");
echo json_encode([
'status' => 'success',
'message' => "Cache for agent {$agentId} is cleared."
]);
}
// 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.");
echo json_encode([
'status' => 'success',
'message' => "Cache for agent {$agentId} is stored."
]);
}
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');
}
// Get host details for this platform
$hostDetails = $hostObject->getHostDetails($platform_id);
// 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;
}
// 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';
}

View File

@ -8,26 +8,46 @@
* 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 = 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']);
Feedback::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
@ -40,15 +60,9 @@ if ($response['db'] === null) {
// Component events listings
//
// list of all component events (default)
require '../app/classes/component.php';
$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);
@ -57,7 +71,7 @@ if ($response['db'] === null) {
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$totalPages = ceil($item_count / $items_per_page);
$components = array();
$components['records'] = array();
@ -78,30 +92,18 @@ if ($response['db'] === null) {
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'AllComponents';
$widget['filter'] = true;
$widget['pagination'] = true;
// widget title
// filter message
$filterMessage = array();
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
$widget['title'] = 'Jitsi events for component&nbsp;<strong>' . $_REQUEST['name'] . '</strong>';
array_push($filterMessage, 'Jitsi events for component&nbsp;"<strong>' . $_REQUEST['name'] . '</strong>"');
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
$widget['title'] = 'Jitsi events for component ID&nbsp;<strong>' . $_REQUEST['id'] . '</strong>';
} else {
$widget['title'] = 'Jitsi events for&nbsp;<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'];
array_push($filterMessage, 'Jitsi events for component ID&nbsp;"<strong>' . $_REQUEST['id'] . '</strong>"');
}
// Get any new feedback messages
include '../app/helpers/feedback.php';
// display the widget
include '../app/templates/event-list-components.php';
include '../app/templates/components.php';
}
?>

View File

@ -8,18 +8,12 @@
* 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 = 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']);
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
// otherwise if DB connection is OK, go on
} else {
@ -52,13 +46,31 @@ 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 = 15;
$browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
$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']);
}
// search and list specific conference ID
if (isset($conferenceId)) {
@ -77,7 +89,7 @@ if ($response['db'] === null) {
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$totalPages = ceil($item_count / $items_per_page);
$conferences = array();
$conferences['records'] = array();
@ -139,32 +151,18 @@ if ($response['db'] === null) {
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'Conferences';
$widget['collapsible'] = false;
$widget['collapsed'] = false;
$widget['filter'] = true;
$widget['pagination'] = true;
// widget title
// filter message
$filterMessage = array();
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
$widget['title'] = 'Conferences with name matching "<strong>' . $_REQUEST['name'] . '"</strong>';
array_push($filterMessage, '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'];
array_push($filterMessage, 'Conference with ID "<strong>' . $_REQUEST['id'] . '</strong>"');
}
// Get any new feedback messages
include '../app/helpers/feedback.php';
// display the widget
include '../app/templates/event-list-conferences.php';
include '../app/templates/conferences.php';
}
?>

View File

@ -1,257 +1,99 @@
<?php
/**
* Configuration management.
* Config management.
*
* This page ("config") handles configuration by adding, editing, and deleting platforms,
* hosts, agents, and the configuration file itself.
* This page handles the config file.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
$action = $_REQUEST['action'] ?? '';
$agent = $_REQUEST['agent'] ?? '';
$host = $_REQUEST['host'] ?? '';
// Get any new feedback messages
include '../app/helpers/feedback.php';
require '../app/classes/config.php';
require '../app/classes/host.php';
require '../app/classes/agent.php';
$configObject = new Config();
$hostObject = new Host($dbWeb);
$agentObject = new Agent($dbWeb);
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
/**
* Handles form submissions from editing page
*/
require '../app/includes/rate_limit_middleware.php';
// 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 {
$result = $configObject->editConfigFile($_POST, $config_file);
if ($result === true) {
$_SESSION['notice'] = "The config file is edited.";
} else {
$_SESSION['error'] = "Editing the config file failed. Error: $result";
}
// For AJAX requests
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
// 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 ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Apply rate limiting
checkRateLimit($dbWeb, 'config', $user_id);
// Ensure no output before this point
ob_clean();
// For AJAX requests, get JSON data
if ($isAjax) {
header('Content-Type: application/json');
// Get raw input
$jsonData = file_get_contents('php://input');
$postData = json_decode($jsonData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$error = json_last_error_msg();
Feedback::flash('ERROR', 'DEFAULT', 'Invalid JSON data received: ' . $error, true);
echo json_encode([
'success' => false,
'message' => 'Invalid JSON data received: ' . $error
]);
exit;
}
// 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);
// Try to update config file
$result = $configObject->editConfigFile($postData, $config_file);
if ($result === true) {
$_SESSION['notice'] = "New Jilo host added.";
$messageData = Feedback::getMessageData('NOTICE', 'DEFAULT', 'Config file updated successfully', true);
echo json_encode([
'success' => true,
'message' => 'Config file updated successfully',
'messageData' => $messageData
]);
} else {
$_SESSION['error'] = "Adding the host failed. Error: $result";
$messageData = Feedback::getMessageData('ERROR', 'DEFAULT', "Error updating config file: $result", true);
echo json_encode([
'success' => false,
'message' => "Error updating config file: $result",
'messageData' => $messageData
]);
}
// 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";
}
exit;
}
// FIXME the new file is not loaded on first page load
unset($config);
header("Location: $app_root?page=config&item=$item");
exit();
// Handle non-AJAX POST
$result = $configObject->editConfigFile($_POST, $config_file);
if ($result === true) {
Feedback::flash('NOTICE', 'DEFAULT', 'Config file updated successfully', true);
} else {
Feedback::flash('ERROR', 'DEFAULT', "Error updating config file: $result", true);
}
} else {
header('Location: ' . htmlspecialchars($app_root) . '?page=config');
exit;
}
// Only include template for non-AJAX requests
if (!$isAjax) {
/**
* Handles GET requests to display templates.
*/
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 {
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();
if ($userObject->hasRight($user_id, 'view config file')) {
include '../app/templates/config.php';
} else {
$logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", 'system');
include '../app/templates/error-unauthorized.php';
}
}
?>

View File

@ -9,9 +9,8 @@
* 3. The most recent 10 conferences.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Get any new feedback messages
include '../app/helpers/feedback.php';
require '../app/classes/conference.php';
require '../app/classes/participant.php';
@ -21,7 +20,7 @@ $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $pl
// if DB connection has error, display it and stop here
if ($response['db'] === null) {
Messages::flash('ERROR', 'DEFAULT', $response['error']);
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
// otherwise if DB connection is OK, go on
} else {
@ -228,5 +227,3 @@ if ($response['db'] === null) {
include '../app/templates/widget.php';
}
?>

View File

@ -1,207 +0,0 @@
<?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:
}
}
?>

View File

@ -0,0 +1,91 @@
<?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($dbWeb);
$hostObject = new Host($dbWeb);
// Connect to Jilo database for log data
$response = connectDB($config, 'jilo', $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';

View File

@ -1,9 +1,6 @@
<?php
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Get any new feedback messages
include '../app/helpers/feedback.php';
include '../app/templates/help-main.php';
?>
include '../app/templates/help.php';

View File

@ -0,0 +1,106 @@
<?php
require '../app/classes/agent.php';
require '../app/classes/host.php';
$agentObject = new Agent($dbWeb);
$hostObject = new Host($dbWeb);
// 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';

View File

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

View File

@ -19,46 +19,75 @@ unset($error);
try {
// connect to database
$dbWeb = connectDB($config);
$dbWeb = connectDB($config)['db'];
// Initialize RateLimiter
require_once '../app/classes/ratelimiter.php';
$rateLimiter = new RateLimiter($dbWeb['db']);
$rateLimiter = new RateLimiter($dbWeb);
// Get user IP
$user_IP = getUserIP();
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
try {
$username = $_POST['username'];
$password = $_POST['password'];
// Validate form data
$security = SecurityHelper::getInstance();
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'remember_me', 'csrf_token']);
// Check if IP is blacklisted
if ($rateLimiter->isIpBlacklisted($user_IP)) {
throw new Exception(Messages::get('LOGIN', 'IP_BLACKLISTED')['message']);
$validationRules = [
'username' => [
'type' => 'string',
'required' => true,
'min' => 3,
'max' => 20
],
'password' => [
'type' => 'string',
'required' => true,
'min' => 2
]
];
$errors = $security->validateFormData($formData, $validationRules);
if (!empty($errors)) {
throw new Exception("Invalid input: " . implode(", ", $errors));
}
// Check rate limiting (but skip if IP is whitelisted)
$username = $formData['username'];
$password = $formData['password'];
// Skip all checks 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']);
// Check if IP is blacklisted
if ($rateLimiter->isIpBlacklisted($user_IP)) {
throw new Exception(Feedback::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']);
}
// Record this attempt
$rateLimiter->attempt($username, $user_IP);
}
// login successful
if ( $userObject->login($username, $password) ) {
// if remember_me is checked, max out the session
if (isset($_POST['remember_me'])) {
if (isset($formData['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;
}
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
// set session lifetime and cookies
setcookie('username', $username, [
'expires' => $setcookie_lifetime,
@ -69,20 +98,28 @@ try {
'samesite' => 'Strict'
]);
// Set session variables
$_SESSION['USER_ID'] = $userObject->getUserId($username)[0]['id'];
$_SESSION['USERNAME'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
if (isset($formData['remember_me'])) {
$_SESSION['REMEMBER_ME'] = true;
}
// 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);
Feedback::flash('LOGIN', 'LOGIN_SUCCESS');
header('Location: ' . htmlspecialchars($app_root));
exit();
} 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
Messages::flash('ERROR', 'DEFAULT', $e->getMessage());
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
if (isset($username)) {
$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');
@ -90,19 +127,16 @@ try {
}
}
} catch (Exception $e) {
Messages::flash('ERROR', 'DEFAULT', 'There was an unexpected error. Please try again.');
Feedback::flash('ERROR', 'DEFAULT');
}
// Show configured login message if any
if (!empty($config['login_message'])) {
echo Messages::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false);
echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false);
}
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Get any new feedback messages
include '../app/helpers/feedback.php';
// Load the template
include '../app/templates/form-login.php';
?>

View File

@ -3,58 +3,98 @@
/**
* 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.
* This page ("logs") retrieves and displays logs within a time range
* either for a specified user or for all users.
* It supports pagination and filtering.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Get any new feedback messages
include '../app/helpers/feedback.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';
$has_system_access = ($userObject->hasRight($user_id, 'superuser') ||
$userObject->hasRight($user_id, 'view app logs'));
// Get current page for pagination
$currentPage = $_REQUEST['page_num'] ?? 1;
$currentPage = (int)$currentPage;
// Get selected tab
$selected_tab = $_REQUEST['tab'] ?? 'user';
if ($selected_tab === 'system' && !$has_system_access) {
$selected_tab = 'user';
}
// Set scope based on selected tab
$scope = ($selected_tab === 'system') ? 'system' : 'user';
// specify time range
include '../app/helpers/time_range.php';
// Prepare search filters
$filters = [];
if (isset($_REQUEST['from_time']) && !empty($_REQUEST['from_time'])) {
$filters['from_time'] = $_REQUEST['from_time'];
}
if (isset($_REQUEST['until_time']) && !empty($_REQUEST['until_time'])) {
$filters['until_time'] = $_REQUEST['until_time'];
}
if (isset($_REQUEST['message']) && !empty($_REQUEST['message'])) {
$filters['message'] = $_REQUEST['message'];
}
if ($scope === 'system' && isset($_REQUEST['id']) && !empty($_REQUEST['id'])) {
$filters['id'] = $_REQUEST['id'];
}
// pagination variables
$items_per_page = 15;
$browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
$offset = ($currentPage - 1) * $items_per_page;
// Build params for pagination
$params = '';
if (!empty($_REQUEST['from_time'])) {
$params .= '&from_time=' . urlencode($_REQUEST['from_time']);
}
if (!empty($_REQUEST['until_time'])) {
$params .= '&until_time=' . urlencode($_REQUEST['until_time']);
}
if (!empty($_REQUEST['message'])) {
$params .= '&message=' . urlencode($_REQUEST['message']);
}
if (!empty($_REQUEST['id'])) {
$params .= '&id=' . urlencode($_REQUEST['id']);
}
if (isset($_REQUEST['tab'])) {
$params .= '&tab=' . urlencode($_REQUEST['tab']);
}
// prepare the result
$search = $logObject->readLog($user_id, $scope, $offset, $items_per_page);
$search_all = $logObject->readLog($user_id, $scope);
$search = $logObject->readLog($user_id, $scope, $offset, $items_per_page, $filters);
$search_all = $logObject->readLog($user_id, $scope, 0, 0, $filters);
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$totalPages = ceil($item_count / $items_per_page);
$logs = array();
$logs['records'] = array();
foreach ($search as $item) {
// when we show only user's logs, omit user_id column
if ($scope === 'user') {
$log_record = array(
// assign title to the field in the array record
'time' => $item['time'],
'log message' => $item['message']
'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']
'userID' => $item['user_id'],
'username' => $item['username'],
'time' => $item['time'],
'log message' => $item['message']
);
}
@ -63,21 +103,7 @@ if (!empty($search)) {
}
}
// 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';
?>
// Load the template
include '../app/templates/logs.php';

View File

@ -8,18 +8,12 @@
* 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 = 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']);
Feedback::flash('ERROR', 'DEFAULT', $response['error']);
// otherwise if DB connection is OK, go on
} else {
@ -52,13 +46,34 @@ 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 = 15;
$browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
$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']);
}
// search and list specific participant ID
if (isset($participantId)) {
@ -82,7 +97,7 @@ if ($response['db'] === null) {
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$totalPages = ceil($item_count / $items_per_page);
$participants = array();
$participants['records'] = array();
@ -144,34 +159,20 @@ if ($response['db'] === null) {
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'Participants';
$widget['collapsible'] = false;
$widget['collapsed'] = false;
$widget['filter'] = true;
$widget['pagination'] = true;
// widget title
// filter message
$filterMessage = array();
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
$widget['title'] = 'Conferences with participant name (stats_id) matching "<strong>' . $_REQUEST['name'] . '"</strong>';
array_push($filterMessage, '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>';
array_push($filterMessage, 'Conferences 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'];
array_push($filterMessage, 'Conferences with participant IP matching "<strong>' . $participantIp . '</strong>"');
}
// Get any new feedback messages
include '../app/helpers/feedback.php';
// display the widget
include '../app/templates/widget.php';
include '../app/templates/participants.php';
}
?>

View File

@ -12,24 +12,40 @@
* - `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'] ?? '';
// if a form is submitted, it's from the edit page
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
require_once '../app/classes/validator.php';
// Apply rate limiting for profile operations
require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'profile', $user_id);
$item = $_REQUEST['item'] ?? '';
// avatar removal
if ($item === 'avatar' && $action === 'remove') {
$validator = new Validator(['user_id' => $user_id]);
$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($user_id, $config['avatars_path'].$userDetails[0]['avatar']);
if ($result === true) {
$_SESSION['notice'] .= "Avatar for user \"{$userDetails[0]['username']}\" is removed. ";
Feedback::flash('NOTICE', 'DEFAULT', "Avatar for user \"{$userDetails[0]['username']}\" is removed.");
} else {
$_SESSION['error'] .= "Removing the avatar failed. Error: $result ";
Feedback::flash('ERROR', 'DEFAULT', "Removing the avatar failed. Error: $result");
}
header("Location: $app_root?page=profile");
@ -37,35 +53,73 @@ 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' => $_POST['name'] ?? '',
'email' => $_POST['email'] ?? '',
'timezone' => $_POST['timezone'] ?? '',
'bio' => $_POST['bio'] ?? '',
];
'name' => htmlspecialchars($_POST['name'] ?? ''),
'email' => filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL),
'timezone' => htmlspecialchars($_POST['timezone'] ?? ''),
'bio' => htmlspecialchars($_POST['bio'] ?? ''),
];
$result = $userObject->editUser($user_id, $updatedUser);
if ($result === true) {
$_SESSION['notice'] .= "User details for \"{$updatedUser['name']}\" are edited. ";
Feedback::flash('NOTICE', 'DEFAULT', "User details for \"{$updatedUser['name']}\" are edited.");
} else {
$_SESSION['error'] .= "Editing the user details failed. Error: $result ";
Feedback::flash('ERROR', 'DEFAULT', "Editing the user details failed. Error: $result");
}
// update the rights
$newRights = $_POST['rights'] ?? array();
// extract the new right_ids
$userRightsIds = array_column($userRights, 'right_id');
// what rights we need to add
$rightsToAdd = array_diff($newRights, $userRightsIds);
if (!empty($rightsToAdd)) {
foreach ($rightsToAdd as $rightId) {
$userObject->addUserRight($user_id, $rightId);
if (isset($_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();
}
}
// what rights we need to remove
$rightsToRemove = array_diff($userRightsIds, $newRights);
if (!empty($rightsToRemove)) {
foreach ($rightsToRemove as $rightId) {
$userObject->removeUserRight($user_id, $rightId);
$newRights = $_POST['rights'];
// extract the new right_ids
$userRightsIds = array_column($userRights, 'right_id');
// what rights we need to add
$rightsToAdd = array_diff($newRights, $userRightsIds);
if (!empty($rightsToAdd)) {
foreach ($rightsToAdd as $rightId) {
$userObject->addUserRight($user_id, $rightId);
}
}
// what rights we need to remove
$rightsToRemove = array_diff($userRightsIds, $newRights);
if (!empty($rightsToRemove)) {
foreach ($rightsToRemove as $rightId) {
$userObject->removeUserRight($user_id, $rightId);
}
}
}
@ -89,12 +143,19 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$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';
}
}
?>

View File

@ -9,46 +9,90 @@
*/
// registration is allowed, go on
if ($config['registration_enabled'] === true) {
if ($config['registration_enabled'] == true) {
try {
// connect to database
$dbWeb = connectDB($config);
global $dbWeb, $logObject, $userObject;
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
$username = $_POST['username'];
$password = $_POST['password'];
// registering
$result = $userObject->register($username, $password);
// Apply rate limiting
require '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'register');
// 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
require_once '../app/classes/validator.php';
require_once '../app/helpers/security.php';
$security = SecurityHelper::getInstance();
// Sanitize input
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'confirm_password', 'csrf_token']);
// Validate CSRF token
if (!$security->verifyCsrfToken($formData['csrf_token'] ?? '')) {
throw new Exception(Feedback::get('ERROR', 'CSRF_INVALID')['message']);
}
$validator = new Validator($formData);
$rules = [
'username' => [
'required' => true,
'min' => 3,
'max' => 20
],
'password' => [
'required' => true,
'min' => 8,
'max' => 100
],
'confirm_password' => [
'required' => true,
'matches' => 'password'
]
];
$username = $formData['username'] ?? 'unknown';
if ($validator->validate($rules)) {
$password = $formData['password'];
// registering
$result = $userObject->register($username, $password);
// redirect to login
if ($result === true) {
// Get the new user's ID for logging
$user_id = $userObject->getUserId($username)[0]['id'];
$logObject->insertLog($user_id, "Registration: New user \"$username\" registered successfully. IP: $user_IP", 'user');
Feedback::flash('NOTICE', 'DEFAULT', "Registration successful. You can log in now.");
header('Location: ' . htmlspecialchars($app_root));
exit();
// registration fail, redirect to login
} else {
$logObject->insertLog(0, "Registration: Failed registration attempt for user \"$username\". IP: $user_IP. Reason: $result", 'system');
Feedback::flash('ERROR', 'DEFAULT', "Registration failed. $result");
header('Location: ' . htmlspecialchars($app_root));
exit();
}
} else {
Messages::flash('ERROR', 'DEFAULT', "Registration failed. $result");
header('Location: ' . htmlspecialchars($app_root));
$error = $validator->getFirstError();
$logObject->insertLog(0, "Registration: Failed validation for user \"" . ($username ?? 'unknown') . "\". IP: $user_IP. Reason: $error", 'system');
Feedback::flash('ERROR', 'DEFAULT', $error);
header('Location: ' . htmlspecialchars($app_root . '?page=register'));
exit();
}
}
} catch (Exception $e) {
Messages::flash('ERROR', 'DEFAULT', $e->getMessage());
$logObject->insertLog(0, "Registration: System error. IP: $user_IP. Error: " . $e->getMessage(), 'system');
Feedback::flash('ERROR', 'DEFAULT', $e->getMessage());
}
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Get any new feedback messages
include '../app/helpers/feedback.php';
// Load the template
include '../app/templates/form-register.php';
// registration disabled
} else {
echo Messages::render('NOTICE', 'DEFAULT', 'Registration is disabled', false);
echo Feedback::render('NOTICE', 'DEFAULT', 'Registration is disabled', false);
}
?>

View File

@ -23,91 +23,157 @@ $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($dbWeb, 'security', $user_id);
$action = $_POST['action'];
$validator = new Validator($_POST);
try {
switch ($action) {
case 'add_whitelist':
if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) {
throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']);
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break;
}
if (empty($_POST['ip_address'])) {
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['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, $user_id)) {
Feedback::flash('SECURITY', 'WHITELIST_ADD_FAILED');
} else {
Feedback::flash('SECURITY', 'WHITELIST_ADD_SUCCESS');
}
} 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($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) {
throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']);
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break;
}
if (empty($_POST['ip_address'])) {
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
$rules = [
'ip_address' => [
'required' => true,
'max' => 45,
'ip' => true
]
];
if ($validator->validate($rules)) {
if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $user_id)) {
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED');
} else {
Feedback::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS');
}
} 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($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) {
throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']);
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break;
}
if (empty($_POST['ip_address'])) {
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['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, $user_id, $expiry_hours)) {
Feedback::flash('SECURITY', 'BLACKLIST_ADD_FAILED');
} else {
Feedback::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS');
}
} 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($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) {
throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']);
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
break;
}
if (empty($_POST['ip_address'])) {
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
$rules = [
'ip_address' => [
'required' => true,
'max' => 45,
'ip' => true
]
];
if ($validator->validate($rules)) {
if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $user_id)) {
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED');
} else {
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS');
}
} else {
Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED', $validator->getFirstError());
}
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;
default:
Feedback::flash('ERROR', 'INVALID_ACTION');
}
} catch (Exception $e) {
$messages[] = ['category' => 'SECURITY', 'key' => 'CUSTOM_ERROR', 'custom_message' => $e->getMessage()];
Messages::flash('SECURITY', 'CUSTOM_ERROR', 'custom_message');
Feedback::flash('ERROR', $e->getMessage());
}
if (empty($messages)) {
// Only redirect if there were no errors
header("Location: {$app_root}?page=security&section={$section}");
exit;
}
// Redirect back to the appropriate section
header("Location: $app_root?page=security&section=" . urlencode($section));
exit;
}
// Always show rate limit info message for rate limiting section
if ($section === 'ratelimit') {
$messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO'];
$system_messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO'];
}
// Get current lists
$whitelisted = $rateLimiter->getWhitelistedIps();
$blacklisted = $rateLimiter->getBlacklistedIps();
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Get any new feedback messages
include '../app/helpers/feedback.php';
// Load the template
include '../app/templates/security.php';
?>

View File

@ -0,0 +1,179 @@
<?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($dbWeb);
$agentObject = new Agent($dbWeb);
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($dbWeb, 'profile', $user_id);
// 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($user_id, 'view settings')) {
$jilo_agent_types = $agentObject->getAgentTypes();
include '../app/templates/settings.php';
} else {
include '../app/templates/error-unauthorized.php';
}
}

View File

@ -8,12 +8,13 @@
* It generates output for each platform and agent.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Get any new feedback messages
include '../app/helpers/feedback.php';
require '../app/classes/agent.php';
require '../app/classes/host.php';
$agentObject = new Agent($dbWeb);
$hostObject = new Host($dbWeb);
include '../app/templates/status-server.php';
@ -23,50 +24,58 @@ foreach ($platformsAll as $platform) {
// check if we can connect to the jilo database
$response = connectDB($config, 'jilo', $platform['jilo_database'], $platform['id']);
if ($response['error'] !== null) {
$jilo_database_status = '<span class="text-danger">' . htmlspecialchars($response['error']) . '</span>';
$jilo_database_status = $response['error'];
} else {
$jilo_database_status = '<span class="text-success">OK</span>';
$jilo_database_status = 'Connected';
}
include '../app/templates/status-platform.php';
// fetch agent details for the current platform
$agentDetails = $agentObject->getAgentDetails($platform['id']);
foreach ($agentDetails as $agent) {
$agent_url = parse_url($agent['url']);
$agent_protocol = isset($agent_url['scheme']) ? $agent_url['scheme']: '';
$agent_host = isset($agent_url['host']) ? $agent_url['host']: '';
$agent_port = isset($agent_url['port']) ? $agent_url['port']: '';
// 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']);
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_port = isset($agent_url['port']) ? $agent_url['port']: '';
// we get agent data to check availability
$agent_response = $agentObject->fetchAgent($agent['id'], true);
$agent_data = json_decode($agent_response);
// we get agent data to check availability
$agent_response = $agentObject->fetchAgent($agent['id'], true);
$agent_data = json_decode($agent_response);
// determine agent availability based on response data
if (json_last_error() === JSON_ERROR_NONE) {
$agent_availability = '<span class="text-warning">unknown</span>';
foreach ($agent_data as $key => $value) {
if ($key === 'error') {
$agent_availability = '<span class="text-danger">' . htmlspecialchars($value) . '</span>';
break;
}
if (preg_match('/_state$/', $key)) {
if ($value === 'error') {
$agent_availability = '<span class="text-danger">not running</span>';
// determine agent availability based on response data
if (json_last_error() === JSON_ERROR_NONE) {
$agent_availability = 'unknown';
foreach ($agent_data as $key => $value) {
if ($key === 'error') {
$agent_availability = $value;
break;
}
if ($value === 'running') {
$agent_availability = '<span class="text-success">running</span>';
break;
if (preg_match('/_state$/', $key)) {
if ($value === 'error') {
$agent_availability = 'not running';
break;
}
if ($value === 'running') {
$agent_availability = 'running';
break;
}
}
}
} else {
$agent_availability = 'json error';
}
} else {
$agent_availability = 'json error';
include '../app/templates/status-agent.php';
}
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>";

View File

@ -1,39 +0,0 @@
<!-- 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 } ?>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,27 +0,0 @@
<!-- 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>
&nbsp;&nbsp;
<input type="submit" class="btn btn-danger btn-sm" value="Save" />
</form>
</div>
</div>
<!-- /widget "config file" -->

View File

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

View File

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

View File

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

View File

@ -1,50 +0,0 @@
<!-- 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>
&nbsp;&nbsp;
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
</form>
</div>
</div>
<!-- /widget "hosts" -->

View File

@ -1,32 +0,0 @@
<!-- 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>
&nbsp;&nbsp;
<input type="submit" class="btn btn-danger btn-sm" value="Delete" />
</form>
</div>
</div>
<!-- /widget "hosts" -->

View File

@ -1,52 +0,0 @@
<!-- 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>
&nbsp;&nbsp;
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
</form>
</div>
</div>
<!-- /widget "hosts" -->

View File

@ -1,39 +0,0 @@
<!-- 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']) ?>
&nbsp;
<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' ?>&nbsp;
&nbsp;
<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" -->

View File

@ -1,134 +0,0 @@
<!-- 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>
&nbsp;&nbsp;
<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 &nbsp;<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' ?>&nbsp;
<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" -->

View File

@ -1,51 +0,0 @@
<!-- 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>
&nbsp;&nbsp;
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
</form>
</div>
</div>
<!-- /widget "platforms" -->

View File

@ -1,32 +0,0 @@
<!-- 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>
&nbsp;&nbsp;
<input type="submit" class="btn btn-danger btn-sm" value="Delete" />
</form>
</div>
</div>
<!-- /widget "platforms" -->

View File

@ -1,36 +0,0 @@
<!-- 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>
&nbsp;&nbsp;
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
</form>
</div>
</div>
<!-- /widget "platforms" -->

View File

@ -1,60 +0,0 @@
<!-- 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 &nbsp;<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>
&nbsp;
<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" -->

View File

@ -0,0 +1,208 @@
<!-- config file -->
<div class="container-fluid mt-2">
<div class="row mb-4">
<div class="col-12 mb-4">
<h2 class="mb-0">Configuration</h2>
<small>Jilo Web 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>
Jilo Web app configuration
</h5>
<?php if ($userObject->hasRight($user_id, '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
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() {
const name = $(this).attr('name');
data[name] = $(this).val();
});
// Handle checkboxes
$('#configForm input[type="checkbox"]').each(function() {
const name = $(this).attr('name');
data[name] = $(this).prop('checked') ? '1' : '0';
});
// Handle selects
$('#configForm select').each(function() {
const name = $(this).attr('name');
data[name] = $(this).val();
});
$.ajax({
url: '<?= htmlspecialchars($app_root) ?>?page=config',
method: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
success: function(response) {
// Show message first
if (response.messageData) {
JsMessages.success(response.messageData['message']);
}
// Only update UI if save was successful
if (response.success) {
// Update view mode values
Object.entries(data).forEach(([key, value]) => {
const $item = $(`[name="${key}"]`).closest('.config-item');
const $viewMode = $item.find('.view-mode');
if ($item.length) {
if ($item.find('input[type="checkbox"]').length) {
// Boolean value
const isEnabled = value === '1';
$viewMode.html(`
<span class="badge ${isEnabled ? 'bg-success' : 'bg-secondary'}">
${isEnabled ? 'Enabled' : 'Disabled'}
</span>
`);
} else if ($item.find('select').length) {
// Environment value
$viewMode.html(`
<span class="badge ${value === 'production' ? 'bg-danger' : 'bg-info'}">
${value}
</span>
`);
} else {
// Text value
if (value === '') {
$viewMode.html('<span class="text-muted fst-italic">blank</span>');
} else {
$viewMode.html(`<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');
}
},
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 -->

View File

@ -0,0 +1,4 @@
<?php
$token = SecurityHelper::getInstance()->generateCsrfToken();
?>
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token) ?>" />

View File

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

View File

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

View File

@ -1,76 +0,0 @@
<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']) ?>" -->

View File

@ -1,94 +0,0 @@
<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']) ?>" -->

View File

@ -4,15 +4,23 @@
<div class="card-body">
<p class="card-text"><strong>Welcome to JILO!</strong><br />Please enter login credentials:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=login">
<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>
<br />&nbsp;<br />
<?php include 'csrf_token.php'; ?>
<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 autofocus />
</div>
<div class="form-group mb-3">
<input type="password" class="form-control w-50 mx-auto" name="password" placeholder="Password"
pattern=".{2,}" title="Eight or more characters"
required />
</div>
<div class="form-group mb-3">
<label for="remember_me">
<input type="checkbox" id="remember_me" name="remember_me" />
remember me
</label>
</div>
<input type="submit" class="btn btn-primary" value="Login" />
</form>
</div>

View File

@ -4,10 +4,22 @@
<div class="card-body">
<p class="card-text">Enter credentials for registration:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=register">
<input type="text" name="username" placeholder="Username" required autofocus />
<br />
<input type="password" name="password" placeholder="Password" required />
<br />&nbsp;<br />
<?php include 'csrf_token.php'; ?>
<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 autofocus />
</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="password" class="form-control w-50 mx-auto" name="confirm_password" placeholder="Confirm password"
pattern=".{8,}" title="Eight or more characters"
required />
</div>
<input type="submit" class="btn btn-primary" value="Register" />
</form>
</div>

View File

@ -1,37 +1,37 @@
<div class="row">
<div class="card w-auto bg-light border-light card-body" style="flex-direction: row;"><?= $widget['title'] ?></div>
</div>
<!-- jitsi graphs -->
<div class="container-fluid mt-2">
<div class="row mb-4">
<div class="col-12 mb-4">
<h2 class="mb-0">Jitsi graphs</h2>
<small>usage graphs for platform <strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong></small>
</div>
</div>
<div class="row">
<div class="card w-auto bg-light border-light card-body filter-results">
<div class="btn-group" role="group">
<input type="button" class="button" style="margin-right: 3px;" onclick="setTimeRange('today'); setActive(this)" value="today" />
<input type="button" class="button" style="margin-right: 3px;" onclick="setTimeRange('last2days'); setActive(this)" value="last 2 days" />
<input type="button" class="button active" style="margin-right: 3px;" onclick="setTimeRange('last7days'); setActive(this)" value="last 7 days" />
<input type="button" class="button" style="margin-right: 3px;" onclick="setTimeRange('thisMonth'); setActive(this)" value="month" />
<input type="button" class="button" style="margin-right: 18px;" onclick="setTimeRange('thisYear'); setActive(this)" value="year" />
<input type="date" style="margin-right: 3px;" id="start-date">
<input type="date" style="margin-right: 3px;" id="end-date">
<input type="button" id="custom_range" class="button" onclick="setCustomTimeRange(); setActive(this)" value="custom range" />
</div>
</div>
</div>
<div class="row">
<div class="card w-auto bg-light border-light card-body filter-results">
<div class="btn-group" role="group">
<input type="button" class="button" style="margin-right: 3px;" onclick="setTimeRange('today'); setActive(this)" value="today" />
<input type="button" class="button" style="margin-right: 3px;" onclick="setTimeRange('last2days'); setActive(this)" value="last 2 days" />
<input type="button" class="button active" style="margin-right: 3px;" onclick="setTimeRange('last7days'); setActive(this)" value="last 7 days" />
<input type="button" class="button" style="margin-right: 3px;" onclick="setTimeRange('thisMonth'); setActive(this)" value="month" />
<input type="button" class="button" style="margin-right: 18px;" onclick="setTimeRange('thisYear'); setActive(this)" value="year" />
<input type="date" style="margin-right: 3px;" id="start-date">
<input type="date" style="margin-right: 3px;" id="end-date">
<input type="button" id="custom_range" class="button" onclick="setCustomTimeRange(); setActive(this)" value="custom range" />
</div>
</div>
</div>
</div>
<script>
// Define an array to store all graph instances
var graphs = [];
</script>
<?php
foreach ($graph as $data) {
<?php foreach ($graph as $data) {
include '../app/helpers/graph.php';
}
?>
} ?>
<script>
// Function to update the label and propagate zoom across charts
@ -140,3 +140,5 @@ window.onload = function() {
setTimeRange('last7days');
};
</script>
<!-- /jitsi graphs -->

View File

@ -1,53 +0,0 @@
<!-- help -->
<div class="card text-center w-100 mx-auto">
<p class="h4 card-header">Jilo Help</p>
<div class="card-body">
<div style="text-align: left; font-family: monospace; font-size: 0.75em; white-space: pre-wrap;">
<a href="https://lindeas.com/jilo">Jilo</a> is a software tools suite developed by <a href="https://lindeas.com">Lindeas Ltd.</a> designed to help in maintaining a Jitsi Meet platform.
It consists of several parts meant to run together, although some of them can be used separately.
<hr /><strong>"JILO"</strong>
This is the command-line tool for extracting information about important events from the Jitsi Meet log files, storing them in a database and searching through that database.
Jilo is written in Bash and has very minimal external dependencies. That means that you can run it on almost any Linux system with jitsi log files.
It can either:
- show the data directly in the terminal,
- provide it to an instance of the web application "Jilo Web" for displaying statistics (the Jilo Web server needs to have access to the sqlite database file from Jilo),
- or send the data output to a "Jilo Agent" that can then allow a remote Jilo Web access it.
This way Jilo is always available on each host in the Jitsi Meet platform for quick command-line search, while also providing data for the statistics on a central Jilo Web server, without any need to put additional software on each server.
<hr /><strong>"Jilo Agent"</strong>
The Jilo Agent is a small program, written in Go. It runs on remote servers in the Jitsi Meet platform, and provides info about the operation of the different components of the platform.
It listens for connections from the central Jilo Web app on a special port and the connection is secured with JWT tokens authentication. It can be configured to use HTTPS. In a firewall you only need to allow the agent's port for incoming connections from the Jilo Web server.
All information about the different services (jvb, jicofo, jigasi, nginx, prosody) is transmitted over that single port. This, plus the authentication and the fact that Jilo Agent doesn't need any additional external programs or libraries to operate, make it very easy for deploying and automating the deployment in a large Jitsi Meet setup.
<hr /><strong>"Jilo Web"</strong>
Jilo Web is the web app that combines all the information received from Jilo and Jilo Agents and shows statistics and graphs of the usage, the events and the issues.
It's a multi-user web tool with user levels and access rights integrated into it, making it suitable for the different levels in an enterprise.
The current website you are looking at is running a Jilo Web instance.
<hr /><strong>"Jilo Server"</strong>
Jilo Server is a server component written in Go, meant to work alongside Jilo Web. It is responsible for all automated tasks - health checks, periodic retrieval of data from the remote Jilo Agents, etc.
It generally works on the same machine as the web interface Jilo Web and shares its database, although if needed it could be deployed on a separate machine.
Jilo Web checks for the Jilo Server availability and displays a warning if there is a problem with the server.
</div>
</div>
</div>
<!-- /help -->

View File

@ -0,0 +1,128 @@
<!-- help -->
<div class="container-fluid mt-2">
<div class="row mb-4">
<div class="col-12 mb-4">
<h2 class="mb-0">Help</h2>
<small>about this program</small>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body p-4">
<!-- Introduction -->
<div class="mb-5">
<p class="lead">
<a href="https://lindeas.com/jilo" class="text-decoration-none">Jilo</a> is a software tools suite developed by
<a href="https://lindeas.com" class="text-decoration-none">Lindeas Ltd.</a> designed to help in maintaining a Jitsi Meet platform.
</p>
<p>It consists of several parts meant to run together, although some of them can be used separately.</p>
</div>
<!-- Components -->
<div class="row g-4">
<!-- Jilo CLI -->
<div class="col-12">
<div class="card border bg-light">
<div class="card-header bg-light d-flex align-items-center">
<i class="fas fa-terminal me-2 text-secondary"></i>
<h5 class="card-title mb-0">JILO</h5>
</div>
<div class="card-body">
<p class="card-text">
This is the command-line tool for extracting information about important events from the Jitsi Meet log files,
storing them in a database and searching through that database.
</p>
<p class="card-text">
Jilo is written in Bash and has very minimal external dependencies. That means that you can run it on almost
any Linux system with jitsi log files.
</p>
<div class="mt-3">
<p class="mb-2">It can either:</p>
<ul class="list-unstyled ps-4">
<li><i class="fas fa-check text-success me-2"></i>Show the data directly in the terminal</li>
<li><i class="fas fa-check text-success me-2"></i>Provide it to an instance of "Jilo Web" for displaying statistics</li>
<li><i class="fas fa-check text-success me-2"></i>Send the data output to a "Jilo Agent"</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Jilo Agent -->
<div class="col-12">
<div class="card border bg-light">
<div class="card-header bg-light d-flex align-items-center">
<i class="fas fa-robot me-2 text-secondary"></i>
<h5 class="card-title mb-0">Jilo Agent</h5>
</div>
<div class="card-body">
<p class="card-text">
The Jilo Agent is a small program, written in Go. It runs on remote servers in the Jitsi Meet platform,
and provides info about the operation of the different components of the platform.
</p>
<div class="mt-3">
<h6 class="fw-bold mb-2">Key Features:</h6>
<ul class="list-unstyled ps-4">
<li><i class="fas fa-shield-alt text-primary me-2"></i>Secured with JWT tokens authentication</li>
<li><i class="fas fa-lock text-primary me-2"></i>HTTPS support</li>
<li><i class="fas fa-network-wired text-primary me-2"></i>Single port for all services</li>
<li><i class="fas fa-box text-primary me-2"></i>No external dependencies</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Jilo Web -->
<div class="col-12">
<div class="card border bg-light">
<div class="card-header bg-light d-flex align-items-center">
<i class="fas fa-globe me-2 text-secondary"></i>
<h5 class="card-title mb-0">Jilo Web</h5>
</div>
<div class="card-body">
<p class="card-text">
Jilo Web is the web app that combines all the information received from Jilo and Jilo Agents and shows
statistics and graphs of the usage, the events and the issues.
</p>
<p class="card-text">
It is a multi-user web tool with user levels and access rights integrated into it, making it suitable
for the different levels in an enterprise.
</p>
<div class="alert alert-info mt-3 mb-0">
<i class="fas fa-info-circle me-2"></i>
The current website you are looking at is running a Jilo Web instance.
</div>
</div>
</div>
</div>
<!-- Jilo Server -->
<div class="col-12">
<div class="card border bg-light">
<div class="card-header bg-light d-flex align-items-center">
<i class="fas fa-server me-2 text-secondary"></i>
<h5 class="card-title mb-0">Jilo Server</h5>
</div>
<div class="card-body">
<p class="card-text">
Jilo Server is a server component written in Go, meant to work alongside Jilo Web. It is responsible for
all automated tasks - health checks, periodic retrieval of data from the remote Jilo Agents, etc.
</p>
<p class="card-text">
It generally works on the same machine as the web interface Jilo Web and shares its database, although
if needed it could be deployed on a separate machine.
</p>
<div class="alert alert-warning mt-3 mb-0">
<i class="fas fa-exclamation-triangle me-2"></i>
Jilo Web checks for the Jilo Server availability and displays a warning if there is a problem with the server.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- /help -->

View File

@ -1,58 +0,0 @@
<div class="row">
<div class="card w-auto bg-light border-light card-body" style="flex-direction: row;"><?= $widget['title'] ?></div>
</div>
<div class="collapse show" id="collapse<?= htmlspecialchars($widget['name']) ?>">
<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>
<th scope="col">Metric</th>
<?php foreach ($widget['records'] as $record) { ?>
<th scope="col">
<?= htmlspecialchars($record['table_headers']) ?>
<?php if ($record['timestamp']) { ?>
<br>
<small class="text-muted">as of <?= date('Y-m-d H:i:s', strtotime($record['timestamp'])) ?></small>
<?php } ?>
</th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php foreach ($widget['metrics'] as $section => $section_metrics) { ?>
<tr class="table-secondary">
<th colspan="<?= count($widget['records']) + 1 ?>"><?= htmlspecialchars($section) ?></th>
</tr>
<?php foreach ($section_metrics as $metric => $metricConfig) { ?>
<tr>
<td><?= htmlspecialchars($metricConfig['label']) ?></td>
<?php foreach ($widget['records'] as $record) { ?>
<td>
<?php if (isset($record['metrics'][$section][$metric])) {
$metric_data = $record['metrics'][$section][$metric];
if ($metric_data['link']) { ?>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=<?= htmlspecialchars($metric_data['link']) ?>&from_time=<?= htmlspecialchars($record['timestamp']) ?>&until_time=<?= htmlspecialchars($record['timestamp']) ?>"><?= htmlspecialchars($metric_data['value']) ?></a>
<?php } else { ?>
<?= htmlspecialchars($metric_data['value']) ?>
<?php }
} else { ?>
<span class="text-muted">No data</span>
<?php } ?>
</td>
<?php } ?>
</tr>
<?php } ?>
<?php } ?>
</tbody>
</table>
<?php } else { ?>
<div class="alert alert-info m-3" role="alert">
No data available from any agents. Please check agent configuration and connectivity.
</div>
<?php } ?>
</div>
</div>
<!-- /widget "<?= htmlspecialchars($widget['name']) ?>" -->

View File

@ -0,0 +1,117 @@
<!-- latest data -->
<div class="container-fluid mt-2">
<div class="row mb-4">
<div class="col-12 mb-4">
<h2 class="mb-0">Latest data from Jilo Agents</h2>
<small>gathered for platform <strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong></small>
</div>
</div>
<div class="row">
<div class="mb-4">
<?php if (!empty($hostsData)) { ?>
<?php foreach ($hostsData as $host) { ?>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-network-wired me-2 text-secondary"></i>
<?= htmlspecialchars($host['name']) ?><small class="text-muted"> (<?= htmlspecialchars($host['address']) ?>)</small>
</h5>
</div>
<div class="card-body">
<?php foreach ($host['agents'] as $agent) { ?>
<div class="mb-4">
<h6 class="border-bottom pb-2">
<i class="fas fa-robot me-2 text-secondary"></i>
<?= htmlspecialchars($agent['name']) ?> agent
</h6>
<table class="table table-results table-striped table-hover table-bordered">
<thead class="align-top">
<tr>
<th>Metric</th>
<th>
Latest value
<br>
<small class="text-muted"><?= date('d M Y H:i:s', strtotime($agent['timestamp'])) ?></small>
</th>
<th>
Previous value
<?php
// Find first metric with previous data to get timestamp
$prevTimestamp = null;
foreach ($metrics as $m_section => $m_metrics) {
foreach ($m_metrics as $m_metric => $m_config) {
if (isset($agent['metrics'][$m_section][$m_metric]['previous'])) {
$prevTimestamp = $agent['metrics'][$m_section][$m_metric]['previous']['timestamp'];
break 2;
}
}
}
if ($prevTimestamp) { ?>
<br>
<small class="text-muted"><?= date('d M Y H:i:s', strtotime($prevTimestamp)) ?></small>
<?php } ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($metrics as $section => $section_metrics) { ?>
<?php
// Check if this section has any data for this agent
$hasData = false;
foreach ($section_metrics as $metric => $metricConfig) {
if (isset($agent['metrics'][$section][$metric])) {
$hasData = true;
break;
}
}
if (!$hasData) continue;
?>
<tr class="table-secondary">
<th colspan="3"><?= htmlspecialchars($section) ?></th>
</tr>
<?php foreach ($section_metrics as $metric => $metricConfig) { ?>
<?php if (isset($agent['metrics'][$section][$metric])) {
$metric_data = $agent['metrics'][$section][$metric];
?>
<tr>
<td><?= htmlspecialchars($metricConfig['label']) ?></td>
<td>
<?php if ($metric_data['link']) { ?>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=<?= htmlspecialchars($metric_data['link']) ?>&from_time=<?= htmlspecialchars($metric_data['latest']['timestamp']) ?>&until_time=<?= htmlspecialchars($metric_data['latest']['timestamp']) ?>"><?= htmlspecialchars($metric_data['latest']['value']) ?></a>
<?php } else { ?>
<?= htmlspecialchars($metric_data['latest']['value']) ?>
<?php } ?>
</td>
<td>
<?php if ($metric_data['previous']) { ?>
<?php if ($metric_data['link']) { ?>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=<?= htmlspecialchars($metric_data['link']) ?>&from_time=<?= htmlspecialchars($metric_data['previous']['timestamp']) ?>&until_time=<?= htmlspecialchars($metric_data['previous']['timestamp']) ?>"><?= htmlspecialchars($metric_data['previous']['value']) ?></a>
<?php } else { ?>
<?= htmlspecialchars($metric_data['previous']['value']) ?>
<?php } ?>
<?php } else { ?>
<span class="text-muted">No previous data</span>
<?php } ?>
</td>
</tr>
<?php } ?>
<?php } ?>
<?php } ?>
</tbody>
</table>
</div>
<?php } ?>
</div>
</div>
<?php } ?>
<?php } else { ?>
<div class="alert alert-info m-3" role="alert">
No data available from any agents. Please check agent configuration and connectivity.
</div>
<?php } ?>
</div>
</div>
</div>
<!-- /latest data -->

View File

@ -0,0 +1,39 @@
<!-- remote config "<?= htmlspecialchars($livejsFile) ?>" -->
<div class="container-fluid mt-2">
<div class="row mb-4">
<div class="col-12 mb-4">
<h2 class="mb-0">Remote Jitsi config</h2>
<small>contents of the file "<strong><?= htmlspecialchars($livejsFile) ?></strong>"</small>
</div>
</div>
<div class="row">
<div class="mb-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-file me-2 text-secondary"></i>
<small><span class="text-muted"><?= htmlspecialchars($platformDetails[0]['jitsi_url']) ?>:</span> <?= htmlspecialchars($livejsFile) ?></small>
<span class="card-text">
<?php if ($mode === 'raw') { ?>
<span class="m-3"><a class="btn border btn-primary" href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=livejs&item=<?= htmlspecialchars($livejsFile) ?>">view only active lines</a></span>
<?php } else { ?>
<span class="m-3"><a class="btn border btn-secondary" href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=livejs&item=<?= htmlspecialchars($livejsFile) ?>&mode=raw">view raw file contents</a></span>
<?php } ?>
</span>
</h5>
</div>
<div class="card-body">
<pre class="results">
<?php
echo htmlspecialchars($livejsData);
?>
</pre>
</div>
</div>
</div>
</div>
</div>
<!-- /remote config "<?= htmlspecialchars($livejsFile) ?>" -->

View File

@ -1,26 +0,0 @@
<!-- Logs 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="?page=logs">
<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="user ID"<?php if (isset($_REQUEST['id'])) echo " value=\"" . htmlspecialchars($_REQUEST['id']) . "\"" ?> />
<input type="text" name="message" placeholder="message"<?php if (isset($_REQUEST['message'])) echo " value=\"" . htmlspecialchars($_REQUEST['message']) . "\"" ?> />
<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>
<!-- /Logs filter -->

View File

@ -1,57 +0,0 @@
<div class="row">
<?php if ($widget['collapsible'] === true) { ?>
<a style="text-decoration: none;" data-toggle="collapse" href="#collapse<?= htmlspecialchars($widget['name']) ?>" role="button" aria-expanded="true" aria-controls="collapse<?= htmlspecialchars($widget['name']) ?>">
<div class="card w-auto bg-light card-body" style="flex-direction: row;"><?= htmlspecialchars($widget['title']) ?></div>
<?php } else { ?>
<div class="card w-auto bg-light border-light card-body" style="flex-direction: row;"><?= htmlspecialchars($widget['title']) ?></div>
<?php } ?>
<?php if ($widget['filter'] === true) {
include '../app/templates/logs-filter.php'; } ?>
<?php if ($widget['collapsible'] === true) { ?>
</a>
<?php } ?>
</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" class="th-<?= htmlspecialchars($header) ?>"><?= htmlspecialchars($header) ?></th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php foreach ($widget['table_records'] as $row) { ?>
<tr>
<?php
foreach ($row as $key => $column) {
if ($key === 'user ID' && isset($user_id) && $user_id === $column) { ?>
<td><strong><?= htmlspecialchars($column ?? '') ?></strong></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']) ?>" -->

View File

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

View File

@ -7,12 +7,15 @@
<?php } ?>
<!-- Footer -->
<div id="footer">Jilo Web <?= htmlspecialchars($config['version']) ?> &copy;2024 - web interface for <a href="https://lindeas.com/jilo">Jilo</a></div>
<div id="footer">Jilo Web <?= htmlspecialchars($config['version']) ?> &copy;2024-<?= date('Y') ?> - web interface for <a href="https://lindeas.com/jilo">Jilo</a></div>
<!-- /Footer -->
</div>
<script src="static/sidebar.js"></script>
<?php if (isset($currentUser) && $page !== 'logout') { ?>
<script src="static/js/sidebar.js"></script>
<?php } ?>
<script>
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
@ -39,7 +42,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
</script>
</body>
</html>

View File

@ -2,18 +2,15 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/bootstrap/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css">
<?php if ($page === 'logs') { ?>
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/logs.css">
<?php } ?>
<?php if ($page === 'profile') { ?>
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/profile.css">
<?php } ?>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/libs/bootstrap/bootstrap-5.3.3.min.css">
<script src="<?= htmlspecialchars($app_root) ?>static/libs/jquery/jquery.min.js"></script>
<script src="<?= htmlspecialchars($app_root) ?>static/libs/bootstrap/popper.min.js"></script>
<script src="<?= htmlspecialchars($app_root) ?>static/libs/bootstrap/bootstrap-4.0.0.min.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/main.css">
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/messages.css">
<script src="<?= htmlspecialchars($app_root) ?>static/js/messages.js"></script>
<?php if (isset($currentUser)) { ?>
<script>
// restore sidebar state before the page is rendered
(function () {
@ -23,27 +20,36 @@
}
})();
</script>
<?php if ($page === 'agents') { ?>
<script src="<?= htmlspecialchars($app_root) ?>static/agents.js"></script>
<?php } ?>
<?php if ($page === 'data' && $item === 'graphs') { ?>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.1"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@1.0.0"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@1.2.1/dist/chartjs-plugin-zoom.min.js"></script>
<?php if ($page === 'logs') { ?>
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/logs.css">
<?php } ?>
<?php if ($page === 'profile') { ?>
<link rel="stylesheet" type="text/css" href="<?= htmlspecialchars($app_root) ?>static/css/profile.css">
<?php } ?>
<?php if ($page === 'agents') { ?>
<script src="<?= htmlspecialchars($app_root) ?>static/js/agents.js"></script>
<?php } ?>
<?php if ($page === 'graphs') { ?>
<script src="<?= htmlspecialchars($app_root) ?>static/libs/chartjs/chart.umd.min.js"></script>
<script src="<?= htmlspecialchars($app_root) ?>static/libs/chartjs/moment.min.js"></script>
<script src="<?= htmlspecialchars($app_root) ?>static/libs/chartjs/chartjs-adapter-moment.min.js"></script>
<script src="<?= htmlspecialchars($app_root) ?>static/libs/chartjs/chartjs-plugin-zoom.min.js"></script>
<?php } ?>
<title>Jilo Web</title>
<link rel="icon" type="image/x-icon" href="<?= htmlspecialchars($app_root) ?>static/favicon.ico">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col">
<?php if (isset($messages) && is_array($messages)): ?>
<?php foreach ($messages as $msg): ?>
<?= Messages::render($msg['category'], $msg['key'], $msg['custom_message'] ?? null) ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div id="messages-container" class="container-fluid mt-2"></div>
<div class="container-fluid">
<div class="row">
<div class="col">
<?php if (isset($system_messages) && is_array($system_messages)): ?>
<?php foreach ($system_messages as $msg): ?>
<?= Feedback::render($msg['category'], $msg['key'], $msg['custom_message'] ?? null) ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@
</div>
</div>
<li class="font-weight-light text-uppercase" style="font-size: 0.5em; color: whitesmoke; margin-right: 70px; align-content: center;">version&nbsp;<?= htmlspecialchars($config['version']) ?></li>
<li class="font-weight-light text-uppercase" style="font-size: 0.5em; color: whitesmoke; margin-right: 70px; align-content: center;">version&nbsp;<?= htmlspecialchars($config['version']) ?></li>
<?php if ( isset($_SESSION['username']) ) { ?>

View File

@ -2,13 +2,12 @@
<!-- Sidebar -->
<div class="col-md-3 mb-5 sidebar-wrapper bg-light" id="sidebar">
<div class="text-center" style="border: 1px solid #0dcaf0; height: 22px;" id="time_now">
<div class="text-center" style="border: 1px solid #0dcaf0; height: 22px;" id="time_now">
<?php
$timeNow = new DateTime('now', new DateTimeZone($userTimezone));
?>
<!--span style="vertical-align: top; font-size: 12px;"><?= htmlspecialchars($timeNow->format('d M Y H:i')) ?> <?= htmlspecialchars($userTimezone) ?></span-->
<span style="vertical-align: top; font-size: 12px;"><?= htmlspecialchars($timeNow->format('H:i')) ?>&nbsp;&nbsp;<?= htmlspecialchars($userTimezone) ?></span>
</div>
<span style="vertical-align: top; font-size: 12px;"><?= htmlspecialchars($timeNow->format('H:i')) ?>&nbsp;&nbsp;<?= htmlspecialchars($userTimezone) ?></span>
</div>
<div class="col-4"><button class="btn btn-sm btn-info toggle-sidebar-button" type="button" id="toggleSidebarButton" value=">>"></button></div>
<div class="sidebar-content card ml-3 mt-3">
@ -40,56 +39,46 @@ $timeNow = new DateTime('now', new DateTimeZone($userTimezone));
<li class="list-group-item bg-light" style="border: none;"><p class="text-end mb-0"><small>live data</small></p></li>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=data&item=graphs">
<li class="list-group-item<?php if ($page === 'data' && $item === 'graphs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=graphs">
<li class="list-group-item<?php if ($page === 'graphs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-chart-bar" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="combined graphs"></i>combined graphs
</li>
</a>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=data&item=latest">
<li class="list-group-item<?php if ($page === 'data' && $item === 'latest') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=latest">
<li class="list-group-item<?php if ($page === 'latest') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-list" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="latest data"></i>latest data
</li>
</a>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=data&item=configjs">
<li class="list-group-item<?php if ($page === 'data' && $item === 'configjs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=livejs&item=config.js">
<li class="list-group-item<?php if ($page === 'livejs' && $item === 'config.js') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-tv" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="config.js"></i>config.js
</li>
</a>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=data&item=interfaceconfigjs">
<li class="list-group-item<?php if ($page === 'data' && $item === 'interfaceconfigjs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=livejs&item=interface_config.js">
<li class="list-group-item<?php if ($page === 'livejs' && $item === 'interface_config.js') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-th" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="interface_config.js"></i>interface_config.js
</li>
</a>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=agents">
<li class="list-group-item<?php if ($page === 'agents') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-mask" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="jilo agents"></i>jilo agents
<i class="fas fa-robot" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="jilo agents"></i>jilo agents
</li>
</a>
<li class="list-group-item bg-light" style="border: none;"><p class="text-end mb-0"><small>jitsi platforms config</small></p></li>
<li class="list-group-item bg-light" style="border: none;"><p class="text-end mb-0"><small>jitsi platforms settings</small></p></li>
<a href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform">
<li class="list-group-item<?php if ($page === 'config' && $item === 'platform') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-sitemap" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="configuration"></i>platforms
</li>
</a>
<a href="<?= htmlspecialchars($app_root) ?>?page=config&item=host">
<li class="list-group-item<?php if ($page === 'config' && $item === 'host') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-laptop" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="configuration"></i>hosts
</li>
</a>
<a href="<?= htmlspecialchars($app_root) ?>?page=config&item=endpoint">
<li class="list-group-item<?php if ($page === 'config' && $item === 'endpoint') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-stethoscope" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="configuration"></i>endpoints
<a href="<?= htmlspecialchars($app_root) ?>?page=settings">
<li class="list-group-item<?php if ($page === 'settings') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-cog" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="jilo settings"></i>settings
</li>
</a>
<li class="list-group-item bg-light" style="border: none;"><p class="text-end mb-0"><small>system</small></p></li>
<?php if ($userObject->hasRight($user_id, 'view config file')) {?>
<a href="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file">
<li class="list-group-item<?php if ($page === 'config' && $item === 'config_file') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-wrench" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="configuration"></i>config file
<a href="<?= htmlspecialchars($app_root) ?>?page=config">
<li class="list-group-item<?php if ($page === 'config') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-wrench" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="app config"></i>config
</li>
</a>
<?php } ?>

View File

@ -0,0 +1,78 @@
<?php
/**
* Reusable pagination view/template component
* Required variables:
* $currentPage - Current page number
* $totalPages - Total number of pages
*/
// Ensure required variables are set
if (!isset($currentPage) || !isset($totalPages)) {
return;
}
// Number of page links to show before and after current page
$range = 2;
?>
<?php if ($totalPages > 1): ?>
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center d-flex flex-row gap-1">
<!-- First page -->
<?php if ($currentPage > 1): ?>
<li class="page-item">
<a class="page-link" href="<?= htmlspecialchars($app_root . '?page=' . $page . $params) ?>">First</a>
</li>
<li class="page-item">
<a class="page-link" href="<?= htmlspecialchars($app_root . '?page=' . $page . ($currentPage > 1 ? '&page_num=' . ($currentPage - 1) : '') . $params) ?>">«</a>
</li>
<?php else: ?>
<li class="page-item disabled">
<span class="page-link">First</span>
</li>
<li class="page-item disabled">
<span class="page-link">«</span>
</li>
<?php endif; ?>
<!-- Page numbers -->
<?php
for ($i = 1; $i <= $totalPages; $i++) {
// Show first, last, current page, 2 pages before and after current, and step pages (10, 20, etc.)
if ($i === 1 ||
$i === $totalPages ||
$i === $currentPage ||
$i === $currentPage - 1 ||
$i === $currentPage + 1 ||
$i === $currentPage - 2 ||
$i === $currentPage + 2 ||
($i % 10 === 0 && $i > 10)
) { ?>
<li class="page-item <?= $i === (int)$currentPage ? 'active' : '' ?>">
<a class="page-link" href="<?= htmlspecialchars($app_root . '?page=' . $page . ($i > 1 ? '&page_num=' . $i : '') . $params) ?>"><?= $i ?></a>
</li>
<?php } elseif ($i === $currentPage - 3 || $i === $currentPage + 3) { ?>
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
<?php } ?>
<?php } ?>
<!-- Last page -->
<?php if ($currentPage < $totalPages): ?>
<li class="page-item">
<a class="page-link" href="<?= htmlspecialchars($app_root . '?page=' . $page . '&page_num=' . ($currentPage + 1) . $params) ?>">»</a>
</li>
<li class="page-item">
<a class="page-link" href="<?= htmlspecialchars($app_root . '?page=' . $page . '&page_num=' . $totalPages . $params) ?>">Last</a>
</li>
<?php else: ?>
<li class="page-item disabled">
<span class="page-link">»</span>
</li>
<li class="page-item disabled">
<span class="page-link">Last</span>
</li>
<?php endif; ?>
</ul>
</nav>
<?php endif; ?>

View File

@ -0,0 +1,134 @@
<!-- jitsi participants events -->
<div class="container-fluid mt-2">
<div class="row mb-4">
<div class="col-md-6 mb-5">
<h2 class="mb-0">Jitsi participants events</h2>
<small>log events related to participants in Jitsi Meet conferences</small>
</div>
<div class="row mb-4">
<!-- participant 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="participants">
<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">Participant name</label>
<input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($_REQUEST['name'] ?? '') ?>" placeholder="Participant name">
</div>
<div class="col-md-4">
<input type="text" class="form-control" id="id" name="id" value="<?= htmlspecialchars($_REQUEST['id'] ?? '') ?>" placeholder="Search in participant IDs">
<input type="text" class="form-control" id="ip" name="ip" value="<?= htmlspecialchars($_REQUEST['ip'] ?? '') ?>" placeholder="Search in participant IPs">
</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=participants" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Clear
</a>
</div>
</form>
</div>
</div>
<!-- /participant events filter -->
<!-- participant 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($participants['records'])) { ?>
<div class="table-responsive border">
<table class="table table-results table-hover">
<thead class="table-light">
<tr>
<?php foreach (array_keys($participants['records'][0]) as $header) { ?>
<th scope="col" class="text-nowrap"><?= htmlspecialchars($header) ?></th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php foreach ($participants['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') { ?>
<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') { ?>
<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 === 'participant ID' && isset($participantId) && $participantId === $column) { ?>
<td><strong><?= htmlspecialchars($column ?? '') ?></strong></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 ($key === 'component ID') { ?>
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&id=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
<?php } elseif ($stats_id && $key === 'parameter' && isset($participantName) && $participantName === $column) { ?>
<td><strong><?= htmlspecialchars($column ?? '') ?></strong></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' && isset($participantIp) && $participantIp === $column) { ?>
<td><strong><?= htmlspecialchars($column ?? '') ?></strong></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 } elseif ($key === 'component') { ?>
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&name=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
<?php
} elseif ($key === 'time') { ?>
<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 participant events found for the specified criteria.
</div>
<?php } ?>
</div>
</div>
</div>
</div>
<!-- /jitsi participants events -->

View File

@ -1,223 +1,226 @@
<!-- Security Settings -->
<div class="container">
<div class="row mb-4">
<div class="col">
<h2>Security Settings</h2>
<ul class="nav nav-tabs">
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist')) { ?>
<li class="nav-item">
<a class="nav-link <?= $section === 'whitelist' ? 'active' : '' ?>" href="?page=security&section=whitelist">IP Whitelist</a>
</li>
<?php } ?>
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist')) { ?>
<li class="nav-item">
<a class="nav-link <?= $section === 'blacklist' ? 'active' : '' ?>" href="?page=security&section=blacklist">IP Blacklist</a>
</li>
<?php } ?>
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting')) { ?>
<li class="nav-item">
<a class="nav-link <?= $section === 'ratelimit' ? 'active' : '' ?>" href="?page=security&section=ratelimit">Rate Limiting</a>
</li>
<?php } ?>
</ul>
</div>
</div>
<?php if ($section === 'whitelist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist'))) { ?>
<!-- Whitelist Section -->
<div class="row mb-4">
<div class="col">
<div class="card">
<div class="card-header">
<h3>IP Whitelist</h3>
IP addresses and networks that will always bypass the ratelimiting login checks.
</div>
<div class="card-body">
<form method="POST" class="mb-4">
<input type="hidden" name="action" value="add_whitelist">
<div class="row g-3">
<div class="col-md-4">
<input type="text" class="form-control" name="ip_address" placeholder="IP Address or CIDR" required>
</div>
<div class="col-md-4">
<input type="text" class="form-control" name="description" placeholder="Description">
</div>
<div class="col-md-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="is_network" id="is_network_white">
<label class="form-check-label" for="is_network_white">Is Network</label>
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Add to Whitelist</button>
</div>
<!-- security settings -->
<div class="container-fluid mt-2">
<div class="row mb-4">
<div class="col">
<h2 class="mb-0">Security settings</h2>
<small>network restrictions to control flooding and brute force attacks</small>
<ul class="nav nav-tabs mt-5">
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist')) { ?>
<li class="nav-item">
<a class="nav-link <?= $section === 'whitelist' ? 'active' : '' ?>" href="?page=security&section=whitelist">IP whitelist</a>
</li>
<?php } ?>
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist')) { ?>
<li class="nav-item">
<a class="nav-link <?= $section === 'blacklist' ? 'active' : '' ?>" href="?page=security&section=blacklist">IP blacklist</a>
</li>
<?php } ?>
<?php if ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting')) { ?>
<li class="nav-item">
<a class="nav-link <?= $section === 'ratelimit' ? 'active' : '' ?>" href="?page=security&section=ratelimit">Rate limiting</a>
</li>
<?php } ?>
</ul>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>IP Address</th>
<th>Network</th>
<th>Description</th>
<th>Added By</th>
<th>Added On</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($whitelisted as $ip) { ?>
<tr>
<td><?= htmlspecialchars($ip['ip_address']) ?></td>
<td><?= $ip['is_network'] ? 'Yes' : 'No' ?></td>
<td><?= htmlspecialchars($ip['description']) ?></td>
<td><?= htmlspecialchars($ip['created_by']) ?></td>
<td><?= htmlspecialchars($ip['created_at']) ?></td>
<td>
<form method="POST" style="display: inline;">
<input type="hidden" name="action" value="remove_whitelist">
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from whitelist?')">Remove</button>
</form>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php } ?>
<?php if ($section === 'blacklist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist'))) { ?>
<!-- Blacklist Section -->
<div class="row mb-4">
<div class="col">
<div class="card">
<div class="card-header">
<h3>IP Blacklist</h3>
IP addresses and networks that will always get blocked at login.
</div>
<div class="card-body">
<form method="POST" class="mb-4">
<input type="hidden" name="action" value="add_blacklist">
<div class="row g-3">
<div class="col-md-3">
<input type="text" class="form-control" name="ip_address" placeholder="IP Address or CIDR" required>
</div>
<div class="col-md-3">
<input type="text" class="form-control" name="reason" placeholder="Reason">
</div>
<div class="col-md-2">
<input type="number" class="form-control" name="expiry_hours" placeholder="Expiry (hours)">
</div>
<div class="col-md-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="is_network" id="is_network_black">
<label class="form-check-label" for="is_network_black">Is Network</label>
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Add to Blacklist</button>
</div>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>IP Address</th>
<th>Network</th>
<th>Reason</th>
<th>Added By</th>
<th>Added On</th>
<th>Expires</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($blacklisted as $ip) { ?>
<tr>
<td><?= htmlspecialchars($ip['ip_address']) ?></td>
<td><?= $ip['is_network'] ? 'Yes' : 'No' ?></td>
<td><?= htmlspecialchars($ip['reason']) ?></td>
<td><?= htmlspecialchars($ip['created_by']) ?></td>
<td><?= htmlspecialchars($ip['created_at']) ?></td>
<td><?= $ip['expiry_time'] ? htmlspecialchars($ip['expiry_time']) : 'Never' ?></td>
<td>
<form method="POST" style="display: inline;">
<input type="hidden" name="action" value="remove_blacklist">
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from blacklist?')">Remove</button>
</form>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php } ?>
<?php if ($section === 'ratelimit' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting'))) { ?>
<!-- Rate Limiting Section -->
<div class="row mb-4">
<div class="col">
<div class="card">
<div class="card-header">
<h3>Rate Limiting Settings</h3>
Rate limiting settings control how many failed login attempts are allowed before blocking an IP address.
</div>
<div class="card-body">
<div class="alert alert-info">
<h4>Current Settings</h4>
<ul>
<li>Maximum login attempts: <?= $rateLimiter->maxAttempts ?></li>
<li>Time window: <?= $rateLimiter->decayMinutes ?> minutes</li>
<li>Auto-blacklist threshold: <?= $rateLimiter->autoBlacklistThreshold ?> attempts</li>
<li>Auto-blacklist duration: <?= $rateLimiter->autoBlacklistDuration ?> hours</li>
</ul>
<p class="mb-0">
<small>Note: These settings can be modified in the RateLimiter class configuration.</small>
</p>
</div>
<h4>Recent Failed Login Attempts</h4>
<table class="table">
<thead>
<tr>
<th>IP Address</th>
<th>Username</th>
<th>Attempted At</th>
</tr>
</thead>
<tbody>
<?php
$stmt = $rateLimiter->db->prepare("
SELECT ip_address, username, attempted_at
FROM {$rateLimiter->ratelimitTable}
ORDER BY attempted_at DESC
LIMIT 10
");
$stmt->execute();
$attempts = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($attempts as $attempt) {
?>
<tr>
<td><?= htmlspecialchars($attempt['ip_address']) ?></td>
<td><?= htmlspecialchars($attempt['username']) ?></td>
<td><?= htmlspecialchars($attempt['attempted_at']) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php if ($section === 'whitelist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit whitelist'))) { ?>
<!-- whitelist section -->
<div class="row mb-4">
<div class="col">
<div class="card">
<div class="card-header">
<h3>IP whitelist</h3>
IP addresses and networks that will always bypass the ratelimiting login checks.
</div>
<div class="card-body">
<form method="POST" class="mb-4">
<?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="add_whitelist">
<div class="row g-3">
<div class="col-md-4">
<input type="text" class="form-control" name="ip_address" placeholder="IP address or CIDR" required>
</div>
<div class="col-md-4">
<input type="text" class="form-control" name="description" placeholder="Description">
</div>
<div class="col-md-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="is_network" id="is_network_white">
<label class="form-check-label" for="is_network_white">is network</label>
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Add to whitelist</button>
</div>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>IP address</th>
<th>Network</th>
<th>Description</th>
<th>Added by</th>
<th>Added on</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($whitelisted as $ip) { ?>
<tr>
<td><?= htmlspecialchars($ip['ip_address']) ?></td>
<td><?= $ip['is_network'] ? 'Yes' : 'No' ?></td>
<td><?= htmlspecialchars($ip['description']) ?></td>
<td><?= htmlspecialchars($ip['created_by']) ?></td>
<td><?= htmlspecialchars($ip['created_at']) ?></td>
<td>
<form method="POST" style="display: inline;">
<?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="remove_whitelist">
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from whitelist?')">Remove</button>
</form>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php } ?>
<?php if ($section === 'blacklist' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit blacklist'))) { ?>
<!-- blacklist section -->
<div class="row mb-4">
<div class="col">
<div class="card">
<div class="card-header">
<h3>IP blacklist</h3>
IP addresses and networks that will always get blocked at login.
</div>
<div class="card-body">
<form method="POST" class="mb-4">
<?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="add_blacklist">
<div class="row g-3">
<div class="col-md-3">
<input type="text" class="form-control" name="ip_address" placeholder="IP address or CIDR" required>
</div>
<div class="col-md-3">
<input type="text" class="form-control" name="reason" placeholder="Reason">
</div>
<div class="col-md-2">
<input type="number" class="form-control" name="expiry_hours" placeholder="Expiry (hours)">
</div>
<div class="col-md-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="is_network" id="is_network_black">
<label class="form-check-label" for="is_network_black">is network</label>
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Add to blacklist</button>
</div>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>IP address</th>
<th>Network</th>
<th>Reason</th>
<th>Added by</th>
<th>Added on</th>
<th>Expires</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($blacklisted as $ip) { ?>
<tr>
<td><?= htmlspecialchars($ip['ip_address']) ?></td>
<td><?= $ip['is_network'] ? 'Yes' : 'No' ?></td>
<td><?= htmlspecialchars($ip['reason']) ?></td>
<td><?= htmlspecialchars($ip['created_by']) ?></td>
<td><?= htmlspecialchars($ip['created_at']) ?></td>
<td><?= $ip['expiry_time'] ? htmlspecialchars($ip['expiry_time']) : 'Never' ?></td>
<td>
<form method="POST" style="display: inline;">
<?php include 'csrf_token.php'; ?>
<input type="hidden" name="action" value="remove_blacklist">
<input type="hidden" name="ip_address" value="<?= htmlspecialchars($ip['ip_address']) ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this IP from blacklist?')">Remove</button>
</form>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php } ?>
<?php if ($section === 'ratelimit' && ($userObject->hasRight($user_id, 'superuser') || $userObject->hasRight($user_id, 'edit ratelimiting'))) { ?>
<!-- rate limiting section -->
<div class="row mb-4">
<div class="col">
<div class="card">
<div class="card-header">
<h3>Rate limiting settings</h3>
Rate limiting settings control how many failed login attempts are allowed before blocking an IP address.
</div>
<div class="card-body">
<div class="alert alert-info">
<h4>Current settings</h4>
<ul>
<li>Maximum login attempts: <?= $rateLimiter->maxAttempts ?></li>
<li>Time window: <?= $rateLimiter->decayMinutes ?> minutes</li>
<li>Auto-blacklist threshold: <?= $rateLimiter->autoBlacklistThreshold ?> attempts</li>
<li>Auto-blacklist duration: <?= $rateLimiter->autoBlacklistDuration ?> hours</li>
</ul>
<p class="mb-0">
<small>Note: These settings can be modified in the RateLimiter class configuration.</small>
</p>
</div>
<h4>Recent failed login attempts</h4>
<table class="table">
<thead>
<tr>
<th>IP sddress</th>
<th>Username</th>
<th>Attempted at</th>
</tr>
</thead>
<tbody>
<?php $stmt = $rateLimiter->db->prepare("
SELECT ip_address, username, attempted_at
FROM {$rateLimiter->authRatelimitTable}
ORDER BY attempted_at DESC
LIMIT 10
");
$stmt->execute();
$attempts = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($attempts as $attempt) { ?>
<tr>
<td><?= htmlspecialchars($attempt['ip_address']) ?></td>
<td><?= htmlspecialchars($attempt['username']) ?></td>
<td><?= htmlspecialchars($attempt['attempted_at']) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php } ?>
</div>
</div>
</div>
</div>
<?php } ?>
</div>
<!-- /Security Settings -->
<!-- /security settings -->

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,20 @@
<!-- jilo agent status -->
<div class="card text-center w-75 mx-lef" style="padding-left: 80px;">
<div class="card-body">
<p class="card-text text-left" style="text-align: left;">
Jilo Agent <a href="<?= htmlspecialchars($app_root) ?>?page=config#platform<?= htmlspecialchars($platform['id']) ?>agent<?= htmlspecialchars($agent['id']) ?>"><?= htmlspecialchars($agent['agent_description']) ?></a>:
<strong><?= $agent_availability ?></strong>
<br />
host: <strong><?= htmlspecialchars($agent_host) ?></strong>,
port: <strong><?= htmlspecialchars($agent_port) ?></strong>,
endpoint: <strong><?= htmlspecialchars($agent['agent_endpoint']) ?></strong>
</p>
</div>
</div>
<!-- jilo agent status -->
<div class="d-flex align-items-center flex-wrap border-top p-3">
<div class="d-flex align-items-center me-4">
<i class="fas fa-robot me-2 text-secondary"></i>
<span class="me-2">Jilo agent
<a href="<?= htmlspecialchars($app_root) ?>?page=settings#platform-<?= htmlspecialchars($platform['id']) ?>agent-<?= htmlspecialchars($agent['id']) ?>" class="text-decoration-none">
<?= htmlspecialchars($agent['agent_description']) ?>
</a>:
</span>
<span class="badge <?= $agent_availability === 'running' ? 'bg-success' : 'bg-danger' ?>" title="<?= $agent_availability !== 'running' ? htmlspecialchars($agent_availability) : '' ?>" data-toggle="tooltip" data-placement="right" data-offset="30.0">
<?= $agent_availability === 'running' ? 'Running' : 'Error' ?>
</span>
</div>
<div class="d-flex align-items-center me-4">
<span class="me-4">Host: <strong><?= htmlspecialchars($agent_host) ?></strong></span>
<span class="me-4">Port: <?= isset($agent_port) && $agent_port !== '' ? '<strong>' . htmlspecialchars($agent_port) . '</strong>' : '<em class="small text-muted">not set</em>' ?></span>
<span>Endpoint: <strong><?= htmlspecialchars($agent['agent_endpoint']) ?></strong></span>
</div>
</div>

View File

@ -1,13 +1,20 @@
<!-- jitsi platform status -->
<br />
<div class="card text-center w-75 mx-lef" style="padding-left: 40px;">
<div class="card-body">
<p class="card-text text-left" style="text-align: left;">
Jitsi Meet platform: <a href="<?= htmlspecialchars($app_root) ?>?page=config#platform<?= htmlspecialchars($platform['id']) ?>"><?= htmlspecialchars($platform['name']) ?></a>
<br />
jilo database: <strong><?= htmlspecialchars($platform['jilo_database']) ?></strong>,
status: <strong><?= $jilo_database_status ?></strong>
</p>
</div>
</div>
<!-- jitsi platform status -->
<div class="card mt-3 mb-3">
<div class="card-header">
<h4>
<a href="<?= htmlspecialchars($app_root) ?>?page=settings#platform-<?= htmlspecialchars($platform['id']) ?>" class="text-decoration-none">
Jitsi platform "<?= htmlspecialchars($platform['name']) ?>"
</a>
</h4>
<small class="text-muted">Remote Jitsi Meet installation with its database and agents here.</small>
</div>
<div class="card-body">
<div class="d-flex align-items-center flex-wrap">
<span class="me-4">Jilo database: <strong><?= htmlspecialchars($platform['jilo_database']) ?></strong></span>
<div class="d-flex align-items-center">
<span class="me-2">Status:</span>
<span class="badge <?= $jilo_database_status === 'Connected' ? 'bg-success' : 'bg-danger' ?>"><?= htmlspecialchars($jilo_database_status) ?></span>
</div>
</div>
</div>

View File

@ -1,19 +1,30 @@
<!-- jilo status -->
<div class="card text-center w-75 mx-lef">
<p class="h4 card-header">Jilo platform status</p>
<div class="card-body">
<p class="card-text text-left" style="text-align: left;">
Jilo Server:
<?php if ($server_status) { ?>
<strong><span class="text-success">running</span></strong>
<?php } else { ?>
<strong><span class="text-danger">not running</span></strong>
<?php } ?>
<br />
host: <strong><?= htmlspecialchars($server_host) ?></strong>,
port: <strong><?= htmlspecialchars($server_port) ?></strong>,
endpoint: <strong><?= htmlspecialchars($server_endpoint) ?></strong>
</p>
</div>
</div>
<div class="container-fluid mt-2">
<div class="row mb-4">
<div class="col-md-6 mb-5">
<h2 class="mb-0">Jilo status</h2>
<small>status checks of the whole Jilo monitoring platform</small>
</div>
<div class="row mb-4">
<!-- jilo server status -->
<div class="card mt-3">
<div class="card-header">
<h4>Jilo server</h4>
<small class="text-muted">Responsible for periodic checks of remote agents and storing received data.</small>
</div>
<div class="card-body">
<div class="d-flex align-items-center flex-wrap">
<div class="d-flex align-items-center me-4">
<span class="me-2">Jilo server:</span>
<span class="badge <?= $server_status ? 'bg-success' : 'bg-danger' ?>">
<?= $server_status ? 'Running' : 'Not running' ?>
</span>
</div>
<span class="me-4">Host: <strong><?= htmlspecialchars($server_host) ?></strong></span>
<span class="me-4">Port: <strong><?= htmlspecialchars($server_port) ?></strong></span>
<span>Endpoint: <strong><?= htmlspecialchars($server_endpoint) ?></strong></span>
</div>
</div>
</div>

View File

@ -18,7 +18,11 @@
<!-- 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>
<p class="m-3">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 } ?>
<div class="mb-5">
<?php if ($widget['full'] === true) { ?>

View File

@ -6,8 +6,6 @@
<?php } else { ?>
<div class="card w-auto bg-light border-light card-body" style="flex-direction: row;"><?= $widget['title'] ?></div>
<?php } ?>
<?php if ($widget['filter'] === true) {
include '../app/templates/block-results-filter.php'; } ?>
<?php if ($widget['collapsible'] === true) { ?>
</a>
<?php } ?>
@ -16,7 +14,11 @@
<!-- 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>
<p class="m-3">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 } ?>
<div class="mb-5">
<?php if ($widget['full'] === true) { ?>
@ -24,46 +26,41 @@
<thead class="thead-dark">
<tr>
<?php foreach ($widget['table_headers'] as $header) { ?>
<th scope="col"><?= htmlspecialchars($header) ?></th>
<th scope="col" class="text-nowrap"><?= 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 foreach ($row as $key => $column) {
if ($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') { ?>
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=conferences&name=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
<?php } elseif ($key === 'participant ID' && isset($participantId) && $participantId === $column) { ?>
<td><strong><?= htmlspecialchars($column ?? '') ?></strong></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>
<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 === 'component ID') { ?>
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&id=<?= htmlspecialchars($column ?? '') ?>"><?= htmlspecialchars($column ?? '') ?></a></td>
<?php } elseif ($stats_id && $key === 'parameter' && isset($participantName) && $participantName === $column) { ?>
<td><strong><?= htmlspecialchars($column ?? '') ?></strong></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' && isset($participantIp) && $participantIp === $column) { ?>
<td><strong><?= htmlspecialchars($column ?? '') ?></strong></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 } elseif ($key === 'component') { ?>
<td><a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&name=<?= 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>
<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 }
@ -72,12 +69,6 @@
<?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 } ?>

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