Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • lindeas/jilo-web
1 result
Select Git revision
Show changes
Commits on Source (78)
Showing
with 1476 additions and 19 deletions
......@@ -7,16 +7,44 @@ All notable changes to this project will be documented in this file.
## Unreleased
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.1.1...HEAD
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.1.1...HEAD
- github: https://github.com/lindeas/jilo-web/compare/v0.1.1...HEAD
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.1.1...HEAD
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.2...HEAD
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.2...HEAD
- github: https://github.com/lindeas/jilo-web/compare/v0.2...HEAD
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.2...HEAD
---
## 0.2 - 2024-08-31
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.1.1...v0.2
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.1.1...v0.2
- github: https://github.com/lindeas/jilo-web/compare/v0.1.1...v0.2
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.1.1...v0.2
### Added
- Added collapsible front page widgets
- Added widgets to conferences, participants and components pages
- Added front page widget for monthly conferences and participants number
- Added login/registration control and messages
- Added default config file locations
- Added left collapsible sidebar
- Added logo
- Added database helper functions
- Added support for multiple Jitsi platforms
- Added app environments "production" and "development"
- Added visualisation of config.js and interface_config.js per Jitsi platform
### Changed
- MVC design - models(classes folder), views(templates folder) and controllers(pages folder)
- Changed menus - categories on sidebar menu, jitsi platforms on top menu
- Moved all the app code outside of the public web folder
- Config file now can have nested arrays
### Fixed
- Fixed SQL when conferences start and end time are not explicitly clear
- Web design fixes
- Fixed install script
---
......
......@@ -26,13 +26,15 @@ To see a demo install, go to https://work.lindeas.com/jilo-web-demo/
## version
Current version: **0.1.1** released on **2024-07-25**
Current version: **0.2** released on **2024-08-31**
## license
This project is licensed under the GNU General Public License version 2 (GPL-2.0). See LICENSE file.
Bootstrap is used in this project and is licensed under the MIT License. See bootstrap-license file
Bootstrap is used in this project and is licensed under the MIT License. See license-bootstrap file.
JQuery is used in this project and is licensed under the MIT License. See license-jquery file.
## requirements
......@@ -45,20 +47,29 @@ Bootstrap is used in this project and is licensed under the MIT License. See boo
You can install it in the following ways:
- download the latest release from the **"Releases"** section here
```bash
tar -xzf jilo-web_*.tgz
cd jilo-web/doc/
./install.sh
```
- clone the **git repo**:
```bash
git clone https://github.com/lindeas/jilo-web.git
cd jilo
cd jilo-web
./install.sh
```
- DEB and RPM packages are planned, but still unavailable
- DEB and RPM packages are planned, but not yet available. There are experimental build scripts in "packaging" folder.
## config
- edit index.php and set the system path to the jilo-web.conf.php
- edit jilo-web.conf.php and set all the variables correctly
- "database" is the sqlite db file for jilo-web itself, create it with `cat jilo-web.schema | sqlite3 jilo-web.db`
- "sqlite_file" is the sqlite db file for jilo-web itself, it goes to `app/jilo-web.db`, create it with
```bash
cd app
cat ../doc/jilo-web.schema | sqlite3 jilo-web.db
```
- "jilo_database" is the sqlite db file for jilo, with data from the Jitsi logs
## database
The database is an SQLite file. You also need the sqlite db from jilo (mysql/mariadb support is planned, but still unavailable).
The database is an SQLite file. You also need the sqlite db from jilo (mysql/mariadb support is planned, but not yet available).
......@@ -10,13 +10,13 @@
- - ~~add bootstrap template~~
- - clean up the code to follow model-view--controller style
- - ~~clean up the code to follow model-view--controller style~~
- - no HTML inside PHP code
- - ~~no HTML inside PHP code~~
- - put all additional functions in files in a separate folder
- - reduce try/catch usage, use it only for critical errors
- - ~~reduce try/catch usage, use it only for critical errors~~
- - move all SQL code in the model classes, one query per method
......
......@@ -2,11 +2,9 @@
class Component {
private $db;
private $queries;
public function __construct($database) {
$this->db = $database->getConnection();
$this->queries = include('queries.php');
}
......@@ -21,11 +19,24 @@ class Component {
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
$sql = $this->queries['jitsi_components'];
// 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
AND
(time >= '%s 00:00:00' AND time <= '%s 23:59:59')
ORDER BY
time";
$sql = sprintf($sql, $jitsi_component, $component_id, $from_time, $until_time);
$query = $this->db->prepare($sql);
......
<?php
// all sql queries for the jilo database in one place
class Conference {
private $db;
return [
public function __construct($database) {
$this->db = $database->getConnection();
}
// list of conferences for time period (if given)
// fields: component, duration, conference ID, conference name, number of participants, name count (the conf name is found), conference host
'conferences_all_formatted' => "
SELECT DISTINCT
c.jitsi_component,
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
ce.conference_event = 'conference created')
AS start,
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
ce.conference_event = 'conference expired')
AS end,
c.conference_id,
c.conference_name,
(SELECT COUNT(pe.participant_id)
FROM participant_events pe
WHERE
pe.event_type = 'participant joining'
AND
pe.event_param = c.conference_id) AS participants,
name_counts.name_count,
c.conference_host
FROM
conferences c
JOIN (
SELECT
conference_name,
COUNT(*) AS name_count
FROM
conferences
GROUP BY
conference_name
) AS name_counts ON c.conference_name = name_counts.conference_name
JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE (ce.time >= '%s 00:00:00' AND ce.time <= '%s 23:59:59')
ORDER BY
c.id;",
// search/list specific conference ID
public function conferenceById($conference_id, $from_time, $until_time) {
// time period drill-down
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// search for a conference by its ID for a time period (if given)
'conference_by_id' => "
$sql = "
SELECT
pe.time,
c.conference_id,
......@@ -94,11 +67,35 @@ WHERE
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time;",
pe.time";
$sql = sprintf($sql, $conference_id, $from_time, $until_time, $conference_id, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
// search/list specific conference name
public function conferenceByName($conference_name, $from_time, $until_time) {
// time period drill-down
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// search for a conference by its name for a time period (if given)
'conference_by_name' => "
$sql = "
SELECT
pe.time,
c.conference_id,
......@@ -140,175 +137,157 @@ WHERE
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time;",
pe.time";
$sql = sprintf($sql, $conference_name, $from_time, $until_time, $conference_name, $from_time, $until_time);
// list all participants
'participants_all' => "
SELECT DISTINCT
p.jitsi_component, p.endpoint_id, p.conference_id
FROM
participants p
JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59'
ORDER BY p.id;",
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
// list conferences where participant ID (endpoint_id) is found
'conference_by_participant_id' => "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
p.endpoint_id = '%s'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
participant_id = '%s'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
// list of all conferences
public function conferencesAllFormatted($from_time, $until_time) {
ORDER BY
pe.time;",
// time period drill-down
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// list conferences where participant name (stats_id) is found
'participant_by_stats_id' => "
SELECT
pe.time,
// list of conferences for time period (if given)
// fields: component, duration, conference ID, conference name, number of participants, name count (the conf name is found), conference host
$sql = "
SELECT DISTINCT
c.jitsi_component,
(SELECT COALESCE
(
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
ce.conference_event = 'conference created'
),
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
ce.conference_event = 'bridge selected'
)
)
)
AS start,
(SELECT COALESCE
(
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
(ce.conference_event = 'conference expired' OR ce.conference_event = 'conference stopped')
),
(SELECT pe.time
FROM participant_events pe
WHERE
pe.event_param = c.conference_id
ORDER BY pe.time DESC
LIMIT 1
)
)
)
AS end,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
(SELECT COUNT(pe.participant_id)
FROM participant_events pe
WHERE
pe.event_type = 'participant joining'
AND
pe.event_param = c.conference_id) AS participants,
name_counts.name_count,
c.conference_host
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
pe.event_type = 'stats_id' AND pe.event_param LIKE '%%%s%%'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
JOIN (
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
conference_name,
COUNT(*) AS name_count
FROM
conferences c
LEFT JOIN
conferences
GROUP BY
conference_name
) AS name_counts ON c.conference_name = name_counts.conference_name
JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
event_type = 'stats_id' AND event_param LIKE '%%%s%%'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
WHERE (ce.time >= '%s 00:00:00' AND ce.time <= '%s 23:59:59')
ORDER BY
pe.time;",
c.id";
$sql = sprintf($sql, $from_time, $until_time);
// list conferences where participant IP is found
'participant_by_ip' => "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
pe.event_type = 'pair selected' AND pe.event_param = '%s'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
$query = $this->db->prepare($sql);
$query->execute();
UNION
return $query->fetchAll(PDO::FETCH_ASSOC);
}
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
// number of conferences
public function conferenceNumber($from_time, $until_time) {
// time period drill-down
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// number of conferences for time period (if given)
// NB we need to cross check with first occurrence of "bridge selected"
// as in Jicofo logs there is no way to get the time for conference ID creation
$sql = "
SELECT COUNT(c.conference_id) as conferences
FROM
conferences c
LEFT JOIN (
SELECT ce.conference_id, MIN(ce.time) as first_event_time
FROM conference_events ce
WHERE ce.conference_event = 'bridge selected'
GROUP BY ce.conference_id
) AS first_event ON c.conference_id = first_event.conference_id
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
event_type = 'pair selected' AND event_param = '%s'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
(ce.time >= '%s 00:00:00' AND ce.time <= '%s 23:59:59')
AND (ce.conference_event = 'conference created'
OR (ce.conference_event = 'bridge selected' AND ce.time = first_event.first_event_time)
)";
ORDER BY
pe.time;",
$sql = sprintf($sql, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
// list of jitsi component events
'jitsi_components' => "
SELECT jitsi_component, loglevel, time, component_id, event_type, event_param
FROM
jitsi_components
WHERE
jitsi_component = %s
AND
component_id = %s
AND
(time >= '%s 00:00:00' AND time <= '%s 23:59:59')
ORDER BY
time;",
return $query->fetchAll(PDO::FETCH_ASSOC);
}
];
}
?>
<?php
class Config {
public function getPlatformDetails($config, $platform_id) {
$platformDetails = $config['platforms'][$platform_id];
return $platformDetails;
}
// loading the config.js
public function getPlatformConfigjs($platformDetails, $raw = false) {
// constructing the URL
$configjsFile = $platformDetails['jitsi_url'] . '/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;
}
// loading the interface_config.js
public function getPlatformInterfaceConfigjs($platformDetails, $raw = false) {
// constructing the URL
$interfaceConfigjsFile = $platformDetails['jitsi_url'] . '/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;
}
}
?>
<?php
require '../app/helpers/errors.php';
class Database {
private $pdo;
public function __construct($options) {
// pdo needed
if ( !extension_loaded('pdo') ) {
$error = getError('PDO extension not loaded.');
}
// options check
if (empty($options['type'])) {
$error = getError('Database type is not set.');
}
// database type
switch ($options['type']) {
case 'sqlite':
$this->connectSqlite($options);
break;
case 'mysql' || 'mariadb':
$this->connectMysql($options);
break;
default:
$error = getError("Database type \"{$options['type']}\" is not supported.");
}
}
private function connectSqlite($options) {
// pdo_sqlite extension is needed
if (!extension_loaded('pdo_sqlite')) {
$error = getError('PDO extension for SQLite not loaded.');
}
// SQLite options
if (empty($options['dbFile']) || !file_exists($options['dbFile'])) {
$error = getError("SQLite database file \"{$dbFile}\" not found.");
}
// connect to SQLite
try {
$this->pdo = new PDO("sqlite:" . $options['dbFile']);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
$error = getError('SQLite connection failed: ', $e->getMessage());
}
}
private function connectMysql($options) {
// pdo_mysql extension is needed
if (!extension_loaded('pdo_mysql')) {
$error = getError('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.');
}
// Connect to MySQL
try {
$dsn = "mysql:host={$options['host']};port={$options['port']};dbname={$options['dbname']};charset=utf8";
$this->pdo = new PDO($dsn, $options['user'], $options['password'] ?? '');
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
$error = getError('MySQL connection failed: ', $config['environment'], $e->getMessage());
}
}
public function getConnection() {
return $this->pdo;
}
}
?>
......@@ -2,11 +2,9 @@
class Participant {
private $db;
private $queries;
public function __construct($database) {
$this->db = $database->getConnection();
$this->queries = include('queries.php');
}
......@@ -25,7 +23,52 @@ class Participant {
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
$sql = $this->queries['conference_by_participant_id'];
// list conferences where participant ID (endpoint_id) is found
$sql = "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
p.endpoint_id = '%s'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
participant_id = '%s'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time";
$sql = sprintf($sql, $participant_id, $from_time, $until_time, $participant_id, $from_time, $until_time);
$query = $this->db->prepare($sql);
......@@ -50,7 +93,52 @@ class Participant {
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
$sql = $this->queries['participant_by_stats_id'];
// list conferences where participant name (stats_id) is found
$sql = "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
pe.event_type = 'stats_id' AND pe.event_param LIKE '%%%s%%'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
event_type = 'stats_id' AND event_param LIKE '%%%s%%'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time";
$sql = sprintf($sql, $participant_name, $from_time, $until_time, $participant_name, $from_time, $until_time);
$query = $this->db->prepare($sql);
......@@ -75,7 +163,52 @@ class Participant {
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
$sql = $this->queries['participant_by_ip'];
// list conferences where participant IP is found
$sql = "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
pe.event_type = 'pair selected' AND pe.event_param = '%s'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
event_type = 'pair selected' AND event_param = '%s'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time";
$sql = sprintf($sql, $participant_ip, $from_time, $until_time, $participant_ip, $from_time, $until_time);
$query = $this->db->prepare($sql);
......@@ -85,7 +218,7 @@ class Participant {
}
// list of all conferences
// list of all participants
public function participantsAll($from_time, $until_time) {
// time period drill-down
......@@ -100,7 +233,54 @@ class Participant {
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
$sql = $this->queries['participants_all'];
// list all participants
$sql = "
SELECT DISTINCT
p.jitsi_component, p.endpoint_id, p.conference_id
FROM
participants p
JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59'
ORDER BY p.id";
$sql = sprintf($sql, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
// number of participants
public function participantNumber($from_time, $until_time) {
// time period drill-down
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// number of participants for time period (if given)
$sql = "
SELECT COUNT(DISTINCT p.endpoint_id) as participants
FROM
participants p
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
(pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
AND pe.event_type = 'participant joining'";
$sql = sprintf($sql, $from_time, $until_time);
$query = $this->db->prepare($sql);
......
<?php
class Router {
private $routes = [];
public function add() {
$this->routes[$pattern] = $callback;
}
public function dispatch($url) {
// remove variables from url
$url = strtok($url, '?');
foreach ($this->routes as $pattern => $callback) {
if (preg_match('#^' . $pattern . '$#', $url, $matches)) {
// move any exact match
array_shift($matches);
return $this->invoke($callback, $matches);
}
}
// if there was no match at all, return 404
http_response_code(404);
echo '404 page not found';
}
private function invoke($callback, $params) {
list($controllerName, $methodName) = explode('@', $callback);
// $controllerClass = "\\App\\Controllers\\$controllerName";
$controllerClass = "../pages/$pageName";
$controller = new $controllerClass();
call_user_func_array([$controller, $methodName], $params);
}
}
?>
File moved
<?php
return [
//*******************
// edit to customize
//*******************
// domain for the web app
'domain' => 'localhost',
// subfolder for the web app, if any
'folder' => '/jilo-web/',
// set to false to disable new registrations
'registration_enabled' => true,
// will be displayed on login screen
'login_message' => '',
//*******************************************
// edit only if needed for tests or debugging
//*******************************************
// database
'db' => [
// DB type for the web app, currently only "sqlite" is used
'db_type' => 'sqlite',
// default is ../app/jilo-web.db
'sqlite_file' => '../app/jilo-web.db',
],
// system info
'version' => '0.2',
// development has verbose error messages, production has not
'environment' => 'development',
// *************************************
// Maintained by the app, edit with care
// *************************************
'platforms' => [
'0' => [
'name' => 'lindeas',
'jitsi_url' => 'https://meet.lindeas.com',
'jilo_database' => '../../jilo/jilo-meet.lindeas.db',
],
'1' => [
'name' => 'meet.example.com',
'jitsi_url' => 'https://meet.example.com',
'jilo_database' => '../../jilo/jilo.db',
],
'2' => [
'name' => 'test3',
'jitsi_url' => 'https://test3.example.com',
'jilo_database' => '../../jilo/jilo2.db',
],
],
];
?>
<?php
// Function to format arrays with square brackets
function formatArray(array $array, $indentLevel = 2) {
$indent = str_repeat(' ', $indentLevel); // 4 spaces per indent level
$output = "[\n";
foreach ($array as $key => $value) {
$output .= $indent . "'" . $key . "'" . ' => ';
if (is_array($value)) {
$output .= formatArray($value, $indentLevel + 1);
} else {
$output .= var_export($value, true);
}
$output .= ",\n";
}
$output .= str_repeat(' ', $indentLevel - 1) . ']';
return $output;
}
?>
<?php
// connect to database
function connectDB($config, $database = '', $platform_id = '') {
// connecting ti a jilo sqlite database
if ($database === 'jilo') {
try {
$dbFile = $config['platforms'][$platform_id]['jilo_database'] ?? null;
if (!$dbFile || !file_exists($dbFile)) {
throw new Exception(getError("Invalid platform ID \"{$platform_id}\", database file \"{$dbFile}\"not found."));
}
$db = new Database([
'type' => 'sqlite',
'dbFile' => $dbFile,
]);
} catch (Exception $e) {
$error = getError('Error connecting to DB.', $e->getMessage());
include '../app/templates/block-message.php';
exit();
}
// connecting to a jilo-web database of the web app
} else {
// sqlite database file
if ($config['db']['db_type'] === 'sqlite') {
try {
$db = new Database([
'type' => $config['db']['db_type'],
'dbFile' => $config['db']['sqlite_file'],
]);
$pdo = $db->getConnection();
} catch (Exception $e) {
$error = getError('Error connecting to DB.', $e->getMessage());
include '../app/templates/block-message.php';
exit();
}
// mysql/mariadb database
} elseif ($config['db']['db_type'] === 'mysql' || $config['db']['db_type'] === 'mariadb') {
try {
$db = new Database([
'type' => $config['db']['db_type'],
'host' => $config['db']['sql_host'] ?? 'localhost',
'port' => $config['db']['sql_port'] ?? '3306',
'dbname' => $config['db']['sql_database'],
'user' => $config['db']['sql_username'],
'password' => $config['db']['sql_password'],
]);
$pdo = $db->getConnection();
} catch (Exception $e) {
$error = getError('Error connecting to DB.', $e->getMessage());
include '../app/templates/block-message.php';
exit();
}
// unknown database
} else {
$error = "Error: unknow database type \"{$config['db']['db_type']}\"";
include '../app/templates/block-message.php';
exit();
}
}
return $db;
}
?>
<?php
function getError($message, $error = '', $environment = null) {
global $config;
$environment = $config['environment'] ?? 'production';
if ($environment === 'production') {
return 'There was an unexpected error. Please try again.';
} else {
return $error ?: $message;
}
}
?>
<?php
// render config variables array
function renderConfig($configPart, $indent, $platform=false, $parent='') {
global $app_root;
global $config;
if ($parent === 'platforms') {
?>
<div class="col-md-8 text-start">
<a class="btn btn-secondary" style="padding: 0px;" href="<?= $app_root ?>?page=config&action=add">add</a>
</div>
<div class="border bg-light" style="padding-left: <?= $indent ?>px; padding-bottom: 20px; padding-top: 20px;">
<?php } else {
?>
<div style="padding-left: <?= $indent ?>px; padding-bottom: 20px;">
<?php
}
foreach ($configPart as $config_item => $config_value) {
if ($parent === 'platforms') {
$indent = 0;
}
?>
<div class="row mb-1" style="padding-left: <?= $indent ?>px;">
<div class="col-md-4 text-end">
<?= htmlspecialchars($config_item) ?>:
</div>
<?php
if ($parent === 'platforms') { ?>
<div class="col-md-8 text-start">
<a class="btn btn-secondary" style="padding: 2px;" href="<?= $app_root ?>?platform=<?= htmlspecialchars($config_item) ?>&page=config&action=edit">edit</a>
<?php
// we don't delete the last platform
if (count($configPart) <= 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</span>
<?php } else { ?>
<a class="btn btn-danger" style="padding: 2px;" href="<?= $app_root ?>?platform=<?= htmlspecialchars($config_item) ?>&page=config&action=delete">delete</a>
<?php } ?>
</div>
<?php }
if (is_array($config_value)) {
// here we render recursively nested arrays
$indent = $indent + 50;
if ($parent === 'platforms') {
$indent = 100;
}
if ($config_item === 'platforms') {
renderConfig($config_value, $indent, $platform, 'platforms');
} else {
renderConfig($config_value, $indent, $platform);
}
$indent = 0;
} else {
// if it's not array, just display it
?>
<div class="border col-md-8 text-start">
<?= htmlspecialchars($config_value ?? '')?>
<?= $platform ?>
</div>
<?php
}
?>
</div>
<?php
}
echo '</div>';
}
?>
<?php
$time_range_specified = false;
if (!isset($_REQUEST['from_time']) || (isset($_REQUEST['from_time']) && $_REQUEST['from_time'] == '')) {
$from_time = '0000-01-01';
} else {
$from_time = $_REQUEST['from_time'];
$time_range_specified = true;
}
if (!isset($_REQUEST['until_time']) || (isset($_REQUEST['until_time']) && $_REQUEST['until_time'] == '')) {
$until_time = '9999-12-31';
} else {
$until_time = $_REQUEST['until_time'];
$time_range_specified = true;
}
?>
<?php
require_once '../app/classes/database.php';
require '../app/classes/component.php';
// connect to database
require '../app/helpers/database.php';
$db = connectDB($config, 'jilo', $platform_id);
// specify time range
include '../app/helpers/time_range.php';
// jitsi component events list
// we use $_REQUEST, so that both links and forms work
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
$jitsi_component = "'" . $_REQUEST['name'] . "'";
$component_id = 'component_id';
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
$component_id = "'" . $_REQUEST['id'] . "'";
$jitsi_component = 'jitsi_component';
} else {
// we need the variables to use them later in sql for columnname = columnname
$jitsi_component = 'jitsi_component';
$component_id = 'component_id';
}
//
// Component events listings
//
// list of all component events (default)
$component = new Component($db);
// prepare the result
$search = $component->jitsiComponents($jitsi_component, $component_id, $from_time, $until_time);
if (!empty($search)) {
$components = array();
$components['records'] = array();
foreach ($search as $item) {
extract($item);
$component_record = array(
// assign title to the field in the array record
'component' => $jitsi_component,
'loglevel' => $loglevel,
'time' => $time,
'component ID' => $component_id,
'event' => $event_type,
'param' => $event_param,
);
// populate the result array
array_push($components['records'], $component_record);
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'AllComponents';
$widget['collapsible'] = false;
$widget['collapsed'] = false;
$widget['filter'] = true;
// widget title
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
$widget['title'] = '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'];
}
// display the widget
include('../app/templates/widget.php');
?>
<?php
require_once '../app/classes/database.php';
require '../app/classes/conference.php';
// connect to database
require '../app/helpers/database.php';
$db = connectDB($config, 'jilo', $platform_id);
// specify time range
include '../app/helpers/time_range.php';
// conference id/name are specified when searching specific conference(s)
// either id OR name, id has precedence
// we use $_REQUEST, so that both links and forms work
if (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
$conferenceId = $_REQUEST['id'];
unset($_REQUEST['name']);
unset($conferenceName);
} elseif (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
unset($conferenceId);
$conferenceName = $_REQUEST['name'];
} else {
unset($conferenceId);
unset($conferenceName);
}
//
// Conference listings
//
$conference = new Conference($db);
// search and list specific conference ID
if (isset($conferenceId)) {
$search = $conference->conferenceById($conferenceId, $from_time, $until_time);
// search and list specific conference name
} elseif (isset($conferenceName)) {
$search = $conference->conferenceByName($conferenceName, $from_time, $until_time);
// list of all conferences (default)
} else {
$search = $conference->conferencesAllFormatted($from_time, $until_time);
}
if (!empty($search)) {
$conferences = array();
$conferences['records'] = array();
foreach ($search as $item) {
extract($item);
// we don't have duration field, so we calculate it
if (!empty($start) && !empty($end)) {
$duration = gmdate("H:i:s", abs(strtotime($end) - strtotime($start)));
} else {
$duration = '';
}
// search and list specific conference ID
if (isset($conferenceId)) {
$conference_record = array(
// assign title to the field in the array record
'time' => $time,
'conference ID' => $conference_id,
'conference name' => $conference_name,
'conference host' => $conference_host,
'loglevel' => $loglevel,
'participant ID' => $participant_id,
'event' => $event_type,
'parameter' => $event_param
);
// search and list specific conference name
} elseif (isset($conferenceName)) {
$conference_record = array(
// assign title to the field in the array record
'time' => $time,
'conference ID' => $conference_id,
'conference name' => $conference_name,
'conference host' => $conference_host,
'loglevel' => $loglevel,
'participant ID' => $participant_id,
'event' => $event_type,
'parameter' => $event_param
);
// list of all conferences (default)
} else {
$conference_record = array(
// assign title to the field in the array record
'component' => $jitsi_component,
'start' => $start,
'end' => $end,
'duration' => $duration,
'conference ID' => $conference_id,
'conference name' => $conference_name,
'participants' => $participants,
'name count' => $name_count,
'conference host' => $conference_host
);
}
// populate the result array
array_push($conferences['records'], $conference_record);
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'Conferences';
$widget['collapsible'] = false;
$widget['collapsed'] = false;
$widget['filter'] = true;
// widget title
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
$widget['title'] = 'Conferences with name matching "<strong>' . $_REQUEST['name'] . '"</strong>';
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
$widget['title'] = 'Conference with ID "<strong>' . $_REQUEST['id'] . '"</strong>';
} else {
$widget['title'] = 'All conferences';
}
// widget records
if (!empty($conferences['records'])) {
$widget['full'] = true;
$widget['table_headers'] = array_keys($conferences['records'][0]);
$widget['table_records'] = $conferences['records'];
}
// display the widget
include('../app/templates/widget.php');
?>
<?php
$action = $_REQUEST['action'] ?? '';
require_once '../app/classes/config.php';
require '../app/helpers/errors.php';
require '../app/helpers/config.php';
$configure = new Config();
// if a form is submitted, it's from the edit page
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// load the config file and initialize a copy
$content = file_get_contents($config_file);
$updatedContent = $content;
// new platform adding
if (isset($_POST['new']) && $_POST['new'] === 'true') {
$newPlatform = [
'name' => $_POST['name'],
'jitsi_url' => $_POST['jitsi_url'],
'jilo_database' => $_POST['jilo_database'],
];
// Determine the next available index for the new platform
$nextIndex = count($config['platforms']);
// Add the new platform to the platforms array
$config['platforms'][$nextIndex] = $newPlatform;
// Rebuild the PHP array syntax for the platforms
$platformsArray = formatArray($config['platforms']);
// Replace the platforms section in the config file
$updatedContent = preg_replace(
'/\'platforms\'\s*=>\s*\[[\s\S]+?\],/s',
"'platforms' => {$platformsArray}",
$content
);
$updatedContent = preg_replace('/\s*\]\n/s', "\n", $updatedContent);
// deleting a platform
} elseif (isset($_POST['delete']) && $_POST['delete'] === 'true') {
$platform = $_POST['platform'];
$config['platforms'][$platform]['name'] = $_POST['name'];
$config['platforms'][$platform]['jitsi_url'] = $_POST['jitsi_url'];
$config['platforms'][$platform]['jilo_database'] = $_POST['jilo_database'];
$platformsArray = formatArray($config['platforms'][$platform], 3);
$updatedContent = preg_replace(
"/\s*'$platform'\s*=>\s*\[\s*'name'\s*=>\s*'[^']*',\s*'jitsi_url'\s*=>\s*'[^']*,\s*'jilo_database'\s*=>\s*'[^']*',\s*\],/s",
"",
$content
);
// an update to an existing platform
} else {
$platform = $_POST['platform'];
$config['platforms'][$platform]['name'] = $_POST['name'];
$config['platforms'][$platform]['jitsi_url'] = $_POST['jitsi_url'];
$config['platforms'][$platform]['jilo_database'] = $_POST['jilo_database'];
$platformsArray = formatArray($config['platforms'][$platform], 3);
$updatedContent = preg_replace(
"/\s*'$platform'\s*=>\s*\[\s*'name'\s*=>\s*'[^']*',\s*'jitsi_url'\s*=>\s*'[^']*',\s*'jilo_database'\s*=>\s*'[^']*',\s*\],/s",
"\n '{$platform}' => {$platformsArray},",
$content
);
}
// check if file is writable
if (!is_writable($config_file)) {
$_SESSION['error'] = getError('Configuration file is not writable.');
header("Location: $app_root?platform=$platform_id&page=config");
exit();
}
// try to update the config file
if (file_put_contents($config_file, $updatedContent) !== false) {
// update successful
$_SESSION['notice'] = "Configuration for {$_POST['name']} is updated.";
} else {
// unsuccessful
$error = error_get_last();
$_SESSION['error'] = getError('Error updating the config: ' . ($error['message'] ?? 'unknown error'));
}
// FIXME the new file is not loaded on first page load
unset($config);
header("Location: $app_root?platform=$platform_id&page=config");
exit();
// no form submitted, show the templates
} else {
// $item - config.js and interface_config.js are special case; remote loaded files
switch ($item) {
case 'configjs':
$mode = $_REQUEST['mode'] ?? '';
$raw = ($mode === 'raw');
$platformDetails = $configure->getPlatformDetails($config, $platform_id);
$platformConfigjs = $configure->getPlatformConfigjs($platformDetails, $raw);
include('../app/templates/config-list-configjs.php');
break;
case 'interfaceconfigjs':
$mode = $_REQUEST['mode'] ?? '';
$raw = ($mode === 'raw');
$platformDetails = $configure->getPlatformDetails($config, $platform_id);
$platformInterfaceConfigjs = $configure->getPlatformInterfaceConfigjs($platformDetails, $raw);
include('../app/templates/config-list-interfaceconfigjs.php');
break;
// if there is no $item, we work on the local config file
default:
switch ($action) {
case 'add':
include('../app/templates/config-add-platform.php');
break;
case 'edit':
include('../app/templates/config-edit-platform.php');
break;
case 'delete':
include('../app/templates/config-delete-platform.php');
break;
default:
include('../app/templates/config-list.php');
}
}
}
?>