Compare commits

...

190 Commits
v0.2.1 ... v0.3

Author SHA1 Message Date
Yasen Pramatarov 55ab59372e Prepares for release 0.3 2025-01-15 19:06:12 +02:00
Yasen Pramatarov 80a3068742 updates Changelog 2025-01-15 18:58:29 +02:00
Yasen Pramatarov a5b2653ed4 Removes all old messaging system references 2025-01-15 18:52:15 +02:00
Yasen Pramatarov 378ecb8a14 Adds option to not sanitize messages 2025-01-15 18:22:49 +02:00
Yasen Pramatarov 0cf4795fc7 Fixes variables conflict between config and metricConfig 2025-01-15 18:22:09 +02:00
Yasen Pramatarov 89076d3aeb Fixes sql schemas 2025-01-14 18:01:27 +02:00
Yasen Pramatarov 7e67b2907b Fixes graphs title 2025-01-14 15:46:43 +02:00
Yasen Pramatarov 8b1fd2e2c1 Implements live graphs page 2025-01-14 15:37:20 +02:00
Yasen Pramatarov 5982d5eef9 Adds more start to latest data 2025-01-14 14:33:21 +02:00
Yasen Pramatarov d3f65939cd Makes latest data configurable 2025-01-14 14:09:28 +02:00
Yasen Pramatarov 5986993e45 Adds latest data metrics page 2025-01-13 17:54:42 +02:00
Yasen Pramatarov da4a35d506 Adds the new messages system to all pages 2025-01-13 10:45:31 +02:00
Yasen Pramatarov f1c63de8c0 Logs access fixes 2025-01-07 21:31:07 +02:00
Yasen Pramatarov a8bf994ae5 Messages system troubleshoot 2025-01-07 13:02:57 +02:00
Yasen Pramatarov 708a50bcf8 Fixes login logic 2025-01-06 19:28:19 +02:00
Yasen Pramatarov 01b2c26580 Fixes CSS 2025-01-06 12:09:34 +02:00
Yasen Pramatarov d5e30400d0 Adds smaller size messages 2025-01-06 11:43:20 +02:00
Yasen Pramatarov bc5ae76534 Ratelimiter database call fix 2025-01-06 11:31:07 +02:00
Yasen Pramatarov b314cdd14d Implements and troubleshoots new messages system 2025-01-06 11:13:28 +02:00
Yasen Pramatarov 4bfae911db Reorganizes includes and helpers folders 2025-01-04 18:37:48 +02:00
Yasen Pramatarov 608946ddee Adds new message system with dismissible messages 2025-01-04 14:22:53 +02:00
Yasen Pramatarov eae2a8a47c Adds modal messages, fixes ratelimiter 2025-01-04 13:41:02 +02:00
Yasen Pramatarov 9c129fcf76 Adds a web interface for ratelimiter 2025-01-04 12:30:44 +02:00
Yasen Pramatarov 84354b183d Fixes SQLite syntax and login logic 2025-01-04 11:46:07 +02:00
Yasen Pramatarov 50b74a15db Fixes ratelimiting, adds auto blacklisting 2025-01-04 11:32:19 +02:00
Yasen Pramatarov 8f32a79d0e Loads the ratelimiter class and fixes typos 2025-01-03 18:44:08 +02:00
Yasen Pramatarov a076c28a30 Cleanup expired IP entries 2025-01-03 18:08:59 +02:00
Yasen Pramatarov f4c008c65f Gets blacklisted ips 2025-01-03 18:07:34 +02:00
Yasen Pramatarov 13947e2099 Functions to add/remove from blacklist 2025-01-03 18:06:02 +02:00
Yasen Pramatarov 0a17b947d7 Bypass whitelisting if blacklisted 2025-01-03 18:02:44 +02:00
Yasen Pramatarov 528f4829af Check if blacklisted 2025-01-03 17:58:19 +02:00
Yasen Pramatarov ee920d8e66 SQLite syntax fix 2025-01-03 17:52:21 +02:00
Yasen Pramatarov 65417f7d92 Defines the blacklist table 2025-01-03 17:51:30 +02:00
Yasen Pramatarov 020d0ee22d Adds known bad IPs and networks 2025-01-03 17:49:36 +02:00
Yasen Pramatarov 68f2353c97 Adds blacklist table 2025-01-02 17:46:28 +02:00
Yasen Pramatarov db97101113 Typos fix 2024-12-29 17:35:15 +02:00
Yasen Pramatarov a55a02c209 Adds blacklist table 2024-12-28 17:28:18 +02:00
Yasen Pramatarov 95fb7f06d8 Typo fix 2024-12-23 17:23:35 +02:00
Yasen Pramatarov 589abf2731 Enhances remove from whitelist 2024-12-22 17:21:24 +02:00
Yasen Pramatarov 45d4fbb377 Clean up spaces 2024-12-21 17:18:53 +02:00
Yasen Pramatarov 9b8f92f2eb Enhances add to whitelist 2024-12-21 17:14:31 +02:00
Yasen Pramatarov 8d0518c7ff Fixes sqlite syntax 2024-12-21 17:11:15 +02:00
Yasen Pramatarov d15c6d6f1f Fixes SQL 2024-12-20 17:05:45 +02:00
Yasen Pramatarov 76f4e0e3c8 Prepare for logging 2025-01-03 17:02:49 +02:00
Yasen Pramatarov 0d05d66c0f Default whitelist IPs on table createion 2024-12-17 16:41:23 +02:00
Yasen Pramatarov db6dabedec Remove old type whitelist 2024-12-16 16:39:06 +02:00
Yasen Pramatarov bfa467996f Insert OR ignore in sql 2024-12-16 16:35:36 +02:00
Yasen Pramatarov 2a270dac74 Add local and private IPs to the whitelist 2024-12-13 16:31:25 +02:00
Yasen Pramatarov 667695881c Adds sql to schema file 2024-12-13 16:28:55 +02:00
Yasen Pramatarov bc1089be21 Fixes whitelisting in db 2024-12-12 16:16:48 +02:00
Yasen Pramatarov a0747cfbc8 Adds whitelist table 2024-12-12 16:11:41 +02:00
Yasen Pramatarov 0f72f3bea4 Renames ratelimit table 2024-12-11 16:08:55 +02:00
Yasen Pramatarov 38e4b002c8 Adds comments 2024-12-11 16:00:13 +02:00
Yasen Pramatarov 645e98cd6a Clear extra spaces 2024-12-10 15:56:58 +02:00
Yasen Pramatarov a31939cb87 Skip rate limiting for whitelisted 2024-12-10 15:56:18 +02:00
Yasen Pramatarov 08394be35e Ratelimit whitelist add/remove 2024-12-09 15:44:00 +02:00
Yasen Pramatarov c78951da60 Initial code for ratelimiter whitelist 2024-12-09 15:43:10 +02:00
Yasen Pramatarov f549940249 Adds rate limiting to the login page 2024-12-07 15:38:13 +02:00
Yasen Pramatarov fee0616ca4 Adds rate limiting to the user class 2024-12-07 15:32:28 +02:00
Yasen Pramatarov 626fc4ba2b Adds rate limiter class 2024-12-06 15:25:15 +02:00
Yasen Pramatarov f7e4aeb898 Adds jilo db checks to status page 2024-12-04 16:18:15 +02:00
Yasen Pramatarov 1f7d42b083 Adds phpdoc comments 2024-12-04 12:17:30 +02:00
Yasen Pramatarov 858cc264f1 Adds phpdoc comments 2024-12-04 12:13:33 +02:00
Yasen Pramatarov 7d21406be4 Fixes comments 2024-12-04 12:04:36 +02:00
Yasen Pramatarov d8dc937e48 Adds phpdoc comments 2024-12-04 12:02:06 +02:00
Yasen Pramatarov 06f6a3dfb7 Fixes error message 2024-12-04 11:23:04 +02:00
Yasen Pramatarov da08ad54ca Fixes comments 2024-12-03 11:10:43 +02:00
Yasen Pramatarov b18cca8075 Adds phpdoc comments 2024-12-02 11:06:02 +02:00
Yasen Pramatarov a6b0553393 Fixes formatting 2024-12-01 10:58:42 +02:00
Yasen Pramatarov 0808f573fc Fixes dashboard page db connection 2024-11-30 10:56:22 +02:00
Yasen Pramatarov eb998a555b Cleans up the phpdoc comments 2024-11-29 19:06:27 +02:00
Yasen Pramatarov 7add95dd1b Ad phpdoc comments 2024-11-29 19:02:40 +02:00
Yasen Pramatarov 09c1669812 Adds phpdoc comments 2024-11-29 18:51:52 +02:00
Yasen Pramatarov e57ab435fc Adds phpdoc comments 2024-11-29 18:47:34 +02:00
Yasen Pramatarov 161f74f6bd Cleans up the phpdoc comments 2024-11-29 18:47:18 +02:00
Yasen Pramatarov b94613f049 Cleans up the phpdoc comments 2024-11-29 18:38:49 +02:00
Yasen Pramatarov 28f9fa1007 Adds phpdoc comments 2024-11-29 18:34:54 +02:00
Yasen Pramatarov da75076130 Add phpdoc comments 2024-11-29 18:31:12 +02:00
Yasen Pramatarov 4ba0faf20b Fixes to the url router class 2024-11-29 18:25:09 +02:00
Yasen Pramatarov 2bd6cf89f6 Adds phpdoc comments 2024-11-29 18:21:08 +02:00
Yasen Pramatarov c711d6f011 Cleans up the phpdoc comments 2024-11-29 18:08:56 +02:00
Yasen Pramatarov 3ce20c2069 Clean up the PHPDoc comments 2024-11-29 18:07:47 +02:00
Yasen Pramatarov 866af8acfe Adds phpdoc comments 2024-11-28 16:49:47 +02:00
Yasen Pramatarov a8a85b0666 Adds phpdoc comments 2024-11-28 16:46:13 +02:00
Yasen Pramatarov 831c119636 Adds phpdoc comments 2024-11-28 16:40:50 +02:00
Yasen Pramatarov 3279a565aa Adds phpdoc comments 2024-11-27 16:36:56 +02:00
Yasen Pramatarov 69ac69f41c Adds phpdoc comments 2024-11-27 16:34:16 +02:00
Yasen Pramatarov 4f5557f6ca Adds phpdoc comments 2024-11-27 16:26:55 +02:00
Yasen Pramatarov 06bf414f41 Adds phpdoc comments 2024-11-26 16:22:27 +02:00
Yasen Pramatarov 4b4a9603b9 Clears extra spaces 2024-11-26 16:17:41 +02:00
Yasen Pramatarov 396b449bf2 Adds phpdoc comments 2024-11-26 16:16:16 +02:00
Yasen Pramatarov 9562a7d0bb Adds phpdoc comments 2024-11-25 16:09:47 +02:00
Yasen Pramatarov 51282eae38 Database errors troubleshooting 2024-11-24 16:02:20 +02:00
Yasen Pramatarov d1fe3a7cf5 Adds exceptions for the errors 2024-11-23 15:49:20 +02:00
Yasen Pramatarov fac6e4ea83 Adds phpdoc comments 2024-11-23 15:46:01 +02:00
Yasen Pramatarov c7a161963f Adds phpdoc comments 2024-11-23 15:45:03 +02:00
Yasen Pramatarov 16826b93bf Adds phpdoc comments 2024-11-22 15:33:19 +02:00
Yasen Pramatarov 7ed19a6e48 Adds phpdoc comments 2024-11-21 15:14:07 +02:00
Yasen Pramatarov 77f7e14f78 Adds phpdoc comments 2024-11-20 14:56:47 +02:00
Yasen Pramatarov f48ed80103 Outline selected platform 2024-11-19 14:41:46 +02:00
Yasen Pramatarov 89d4450796 Outline selected platform in config 2024-11-18 14:36:37 +02:00
Yasen Pramatarov 6fcc6da51c Components page: Stop if there is database error 2024-11-17 14:11:22 +02:00
Yasen Pramatarov 43148d3f17 Participants page: Stop if there is DB connection error 2024-11-16 13:08:56 +02:00
Yasen Pramatarov 8c37fea093 Conference page: Stop if there is DB connection error 2024-11-15 13:05:28 +02:00
Yasen Pramatarov 1ad19492f6 Components page DB connect fix 2024-11-14 12:58:41 +02:00
Yasen Pramatarov ade26b7267 Participants page DB connect fix 2024-11-13 12:56:24 +02:00
Yasen Pramatarov 7a1f2b841e Fix conference page db connection 2024-11-12 12:51:33 +02:00
Yasen Pramatarov c5e123ea2f Cleans up error/notice messages 2024-11-11 11:33:02 +02:00
Yasen Pramatarov 96de30f3e0 Error messages fix 2024-11-10 17:59:10 +02:00
Yasen Pramatarov b015ef275f Fixes database connection 2024-11-09 13:06:43 +02:00
Yasen Pramatarov a272d50174 Troubleshooting errors displaying 2024-11-08 12:40:32 +02:00
Yasen Pramatarov d32e270598 Troubleshooting error display 2024-11-08 12:39:38 +02:00
Yasen Pramatarov 686d2c1153 Troubeshoots error displaying 2024-11-08 12:38:19 +02:00
Yasen Pramatarov c245bbb3be Adds check if config file is writable 2024-11-06 18:39:51 +02:00
Yasen Pramatarov c40dc747d3 Removes old code 2024-11-05 18:35:01 +02:00
Yasen Pramatarov 04e89ddca9 Fixes graphs 2024-11-05 18:26:25 +02:00
Yasen Pramatarov f59c758fbc fixes html 2024-11-04 18:22:16 +02:00
Yasen Pramatarov 1aebb7faf9 updates changelog 2024-11-03 18:19:29 +02:00
Yasen Pramatarov 9993f3183c updates changelog 2024-11-03 18:17:21 +02:00
Yasen Pramatarov 55cefffa5e updates changelog 2024-11-03 18:16:23 +02:00
Yasen Pramatarov e97092c46d updates changelog 2024-11-03 18:15:21 +02:00
Yasen Pramatarov 4fcf1b5fec Updates changelog 2024-11-03 18:12:52 +02:00
Yasen Pramatarov 449e65bb91 Updates Changelog 2024-11-02 18:11:31 +02:00
Yasen Pramatarov ca7c8d0909 Updates changelog 2024-11-02 18:10:21 +02:00
Yasen Pramatarov a971ff9bf1 HTML fixes 2024-11-02 18:07:53 +02:00
Yasen Pramatarov 428348bada Cleans up html 2024-11-02 18:03:37 +02:00
Yasen Pramatarov 2e2905e014 Removes old unused code 2024-11-02 17:51:12 +02:00
Yasen Pramatarov 75370231e9 Removes old unused code 2024-11-02 17:47:05 +02:00
Yasen Pramatarov e2fb712098 Removes old unused code 2024-11-02 17:45:28 +02:00
Yasen Pramatarov 422413f45d Removes old unused code 2024-11-02 17:43:00 +02:00
Yasen Pramatarov 8ada088ebb Show boolean field correctly 2024-11-02 17:41:02 +02:00
Yasen Pramatarov 83fd64cf51 Fixes boolean fields 2024-11-02 17:33:18 +02:00
Yasen Pramatarov e90f4ca020 Fixes boolean fields in conf editing 2024-11-01 20:01:43 +02:00
Yasen Pramatarov 5220ab7fcd Disables db_type editing, only sqlite is supported 2024-11-01 19:48:23 +02:00
Yasen Pramatarov 213c627208 Removes old code 2024-11-01 19:44:08 +02:00
Yasen Pramatarov d492bff7b7 Config file editing troubleshooting 2024-11-01 19:42:48 +02:00
Yasen Pramatarov 52e5afffbc Removes unused vars 2024-11-01 19:20:25 +02:00
Yasen Pramatarov 43b6547238 Fixes config editing 2024-11-01 19:18:53 +02:00
Yasen Pramatarov 13ec66548f Typo fix 2024-11-01 18:58:53 +02:00
Yasen Pramatarov a65ead94ad Fixes config editing 2024-11-01 18:27:57 +02:00
Yasen Pramatarov c00c1845f6 Adds editing of the config file 2024-11-01 18:23:40 +02:00
Yasen Pramatarov 7841b13fd8 Adds links to Jitsi platforms in new window 2024-10-31 15:44:10 +02:00
Yasen Pramatarov 7d845b7998 Adds highlight to platform config page 2024-10-31 15:28:59 +02:00
Yasen Pramatarov 1bc6313e98 Fixes sidebar menu 2024-10-31 15:02:28 +02:00
Yasen Pramatarov 0120abf246 HTML fixes 2024-10-31 11:59:17 +02:00
Yasen Pramatarov f8bf21e3e5 Fixes host editing 2024-10-31 11:51:16 +02:00
Yasen Pramatarov 681ba504aa fixes error reporting 2024-10-31 11:45:59 +02:00
Yasen Pramatarov 37642cedaa Fixes platform editing 2024-10-31 11:39:03 +02:00
Yasen Pramatarov 1733e75dd1 Fixes errors and notices 2024-10-31 11:35:41 +02:00
Yasen Pramatarov e084a04305 Fixes host editing 2024-10-31 11:25:37 +02:00
Yasen Pramatarov 5d715e4aac Cleans up the html 2024-10-31 11:08:53 +02:00
Yasen Pramatarov eea3271fe6 Ads host delete page 2024-10-30 22:14:54 +02:00
Yasen Pramatarov ad6b2d0799 Fixes host adding 2024-10-30 22:00:39 +02:00
Yasen Pramatarov 84901a77e0 Fixes add host pages 2024-10-30 19:11:23 +02:00
Yasen Pramatarov d629e2d9d3 Cleans up data configjs pages 2024-10-30 17:23:22 +02:00
Yasen Pramatarov b79a0ac4da Adds hosts config page 2024-10-30 17:21:36 +02:00
Yasen Pramatarov 3c85c16480 Cleans up add platform pages 2024-10-30 17:10:35 +02:00
Yasen Pramatarov 5327a82685 Cleans up platform delete 2024-10-30 17:03:42 +02:00
Yasen Pramatarov d51b16619f Cleans up the config platforms page 2024-10-30 16:54:09 +02:00
Yasen Pramatarov e1265500cf Enhances config file edit form 2024-10-30 15:55:03 +02:00
Yasen Pramatarov b67a915ebe Puts warnings on config file edit page 2024-10-30 15:11:27 +02:00
Yasen Pramatarov 67380b1b3d Fixes config edit page 2024-10-30 13:52:18 +02:00
Yasen Pramatarov 74e9285804 Fixes edit config file pages 2024-10-29 13:48:23 +02:00
Yasen Pramatarov a953dd386d Fixes configfile edit form 2024-10-29 13:45:27 +02:00
Yasen Pramatarov 82962e0449 Adds config file edit page 2024-10-29 13:38:22 +02:00
Yasen Pramatarov 20c36f9d02 HTML fixes 2024-10-29 13:22:03 +02:00
Yasen Pramatarov 15798b08a7 Moves config file to its own page 2024-10-29 13:15:14 +02:00
Yasen Pramatarov ced3ac484d Adds new icons 2024-10-29 11:53:59 +02:00
Yasen Pramatarov 5f6de8d328 Changes the structure of sidebar menu 2024-10-29 11:41:11 +02:00
Yasen Pramatarov 1b03a8dcd1 Removes the old page latest data 2024-10-29 11:36:43 +02:00
Yasen Pramatarov 29f5e89acf MOves latest data to data page 2024-10-29 11:35:43 +02:00
Yasen Pramatarov 7628671241 Removes the old page graphs 2024-10-28 11:32:26 +02:00
Yasen Pramatarov 9a983ecca6 Moves graphs to the data page 2024-10-28 11:30:45 +02:00
Yasen Pramatarov 93a65c0836 Initial rewrite of config pages, adds data page 2024-10-28 11:20:09 +02:00
Yasen Pramatarov e9325ee57b Fixe html 2024-10-26 15:37:35 +03:00
Yasen Pramatarov 27c2682c3f Add an agent only if such type has not already beed added 2024-10-26 15:35:16 +03:00
Yasen Pramatarov be8f008eb4 Adds check for already added agents in a aplatform 2024-10-26 15:19:45 +03:00
Yasen Pramatarov 9bfb79d036 Plural form fix 2024-10-25 12:32:53 +03:00
Yasen Pramatarov b3e3a78ed9 Adds check period to creating a new agent 2024-10-25 12:19:54 +03:00
Yasen Pramatarov 2bf3a423f4 Adds check period to agents config 2024-10-24 12:11:35 +03:00
Yasen Pramatarov 8840efebdb Fixes bugs in URL redirects 2024-10-23 15:28:45 +03:00
Yasen Pramatarov fee54aa827 Checks jilo server only when logged in 2024-10-23 15:13:01 +03:00
Yasen Pramatarov 36f287e169 Fixes json escaping 2024-10-23 13:22:55 +03:00
Yasen Pramatarov 9b45d5df9e Adds status checks for the agents in status page 2024-10-23 13:06:59 +03:00
Yasen Pramatarov 73d429647c Adds agents to the status page 2024-10-22 18:03:34 +03:00
Yasen Pramatarov ed9ce2bb07 Adds agent status check button 2024-10-22 16:59:55 +03:00
Yasen Pramatarov daf41efa4c UPdates the help page with Jilo Server info 2024-10-19 16:40:00 +03:00
Yasen Pramatarov d5bc6a3c8d updates CHANGELOG 2024-10-19 16:20:25 +03:00
Yasen Pramatarov cada9dd67c Adds Jilo status page 2024-10-19 16:09:16 +03:00
Yasen Pramatarov f3457d5240 Adds a Jilo Server check and notice. 2024-10-18 15:41:15 +03:00
80 changed files with 4329 additions and 1051 deletions

View File

@ -4,6 +4,40 @@ All notable changes to this project will be documented in this file.
---
## 0.3 - 2025-01-15
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.2.1...v0.3
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.2.1...v0.3
- github: https://github.com/lindeas/jilo-web/compare/v0.2.1...v0.3
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.2.1...v0.3
### Added
- Added status page
- Added latest data page
- Added graphs page
- Added Jilo agents status checks
- Added periodic Jilo agents checks
- Added Jilo Server check and notice on error
- Added "jitsi platforms config" section in the sidebar
- Added editing for platforms
- Added editing for hosts
- Added editing for the Jilo configuration file
- Added phpdoc comments
- Added rate limiting for login with blacklist and whitelist
- Added a page for configuring the rate limiting
### Changed
- Implemented a new messaging and notifications system
- Moved all live checks pages to the "live data" sidebar section
- Separated the config page to multiple pages
- Moved the config pages to "jitsi platforms config" section
### Fixed
- Fixed bugs in config editing pages and cleaned up the HTML
---
## 0.2.1 - 2024-10-17
#### Links

View File

@ -26,7 +26,7 @@ To see a demo install, go to https://work.lindeas.com/jilo-web-demo/
## version
Current version: **0.2.1** released on **2024-10-17**
Current version: **0.3** released on **2025-01-15**
## license
@ -43,6 +43,7 @@ Chart.js is used in this project and is licensed under the MIT License. See lice
- web server (deb: apache | nginx)
- php support in the web server (deb: php-fpm | libapache2-mod-php)
- pdo and pdo_sqlite support in php (deb: php-db, php-sqlite3) uncomment in php.ini: ;extension=pdo_sqlite
- php-curl module
## installation

View File

@ -1,13 +1,36 @@
<?php
/**
* class Agent
*
* Provides methods to interact with Jilo agents, including retrieving details, managing agents, generating JWT tokens,
* and fetching data from agent APIs.
*/
class Agent {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
/**
* Agent constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
$this->db = $database->getConnection();
}
// get details of a specified agent ID (or all) in a specified platform ID
/**
* Retrieves details of a specified agent ID (or all agents) in a specified platform.
*
* @param int $platform_id The platform ID to filter agents by.
* @param int $agent_id The agent ID to filter by. If empty, all agents are returned.
*
* @return array The list of agent details.
*/
public function getAgentDetails($platform_id, $agent_id = '') {
$sql = 'SELECT
ja.id,
@ -15,6 +38,7 @@ class Agent {
ja.agent_type_id,
ja.url,
ja.secret_key,
ja.check_period,
jat.description AS agent_description,
jat.endpoint AS agent_endpoint
FROM
@ -40,7 +64,44 @@ class Agent {
return $query->fetchAll(PDO::FETCH_ASSOC);
}
// get agent types
/**
* Retrieves details of a specified agent by its agent ID.
*
* @param int $agent_id The agent ID to filter by.
*
* @return array The agent details.
*/
public function getAgentIDDetails($agent_id) {
$sql = 'SELECT
ja.id,
ja.platform_id,
ja.agent_type_id,
ja.url,
ja.secret_key,
ja.check_period,
jat.description AS agent_description,
jat.endpoint AS agent_endpoint
FROM
jilo_agents ja
JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id
WHERE
ja.id = :agent_id';
$query = $this->db->prepare($sql);
$query->bindParam(':agent_id', $agent_id);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Retrieves all agent types.
*
* @return array List of all agent types.
*/
public function getAgentTypes() {
$sql = 'SELECT *
FROM jilo_agent_types
@ -51,13 +112,44 @@ class Agent {
return $query->fetchAll(PDO::FETCH_ASSOC);
}
// add new agent
/**
* Retrieves agent types already configured for a specific platform.
*
* @param int $platform_id The platform ID to filter agents by.
*
* @return array List of agent types configured for the platform.
*/
public function getPlatformAgentTypes($platform_id) {
$sql = 'SELECT
id,
agent_type_id
FROM
jilo_agents
WHERE
platform_id = :platform_id';
$query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Adds a new agent to the platform.
*
* @param int $platform_id The platform ID where the agent is to be added.
* @param array $newAgent The new agent details to add.
*
* @return bool|string Returns true on success or an error message on failure.
*/
public function addAgent($platform_id, $newAgent) {
try {
$sql = 'INSERT INTO jilo_agents
(platform_id, agent_type_id, url, secret_key)
(platform_id, agent_type_id, url, secret_key, check_period)
VALUES
(:platform_id, :agent_type_id, :url, :secret_key)';
(:platform_id, :agent_type_id, :url, :secret_key, :check_period)';
$query = $this->db->prepare($sql);
$query->execute([
@ -65,6 +157,7 @@ class Agent {
':agent_type_id' => $newAgent['type_id'],
':url' => $newAgent['url'],
':secret_key' => $newAgent['secret_key'],
':check_period' => $newAgent['check_period'],
]);
return true;
@ -74,13 +167,22 @@ class Agent {
}
}
// edit an existing agent
/**
* Edits an existing agent's details.
*
* @param int $platform_id The platform ID where the agent exists.
* @param array $updatedAgent The updated agent details.
*
* @return bool|string Returns true on success or an error message on failure.
*/
public function editAgent($platform_id, $updatedAgent) {
try {
$sql = 'UPDATE jilo_agents SET
agent_type_id = :agent_type_id,
url = :url,
secret_key = :secret_key
secret_key = :secret_key,
check_period = :check_period
WHERE
id = :agent_id
AND
@ -91,6 +193,7 @@ class Agent {
':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,
]);
@ -103,7 +206,13 @@ class Agent {
}
// delete an agent
/**
* Deletes an agent from the platform.
*
* @param int $agent_id The agent ID to delete.
*
* @return bool|string Returns true on success or an error message on failure.
*/
public function deleteAgent($agent_id) {
try {
$sql = 'DELETE FROM jilo_agents
@ -122,7 +231,13 @@ class Agent {
}
// check for agent cache
/**
* Checks if the agent cache is still valid.
*
* @param int $agent_id The agent ID to check.
*
* @return bool Returns true if cache is valid, false otherwise.
*/
public function checkAgentCache($agent_id) {
$agent_cache_name = 'agent' . $agent_id . '_cache';
$agent_cache_time = 'agent' . $agent_id . '_time';
@ -130,13 +245,26 @@ class Agent {
}
// method for base64 URL encoding for JWT tokens
/**
* Base64 URL encodes the input data. Used for encoding JWT tokens
*
* @param string $data The data to encode.
*
* @return string The base64 URL encoded string.
*/
private function base64UrlEncode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
// generate a JWT token for jilo agent
/**
* Generates a JWT token for a Jilo agent.
*
* @param array $payload The payload data to include in the token.
* @param string $secret_key The secret key used to sign the token.
*
* @return string The generated JWT token.
*/
public function generateAgentToken($payload, $secret_key) {
// header
@ -161,61 +289,282 @@ class Agent {
}
// fetch result from jilo agent API
/**
* Fetches data from a Jilo agent's API, optionally forcing a refresh of the cache.
*
* @param int $agent_id The agent ID to fetch data for.
* @param bool $force Whether to force-refresh the cache (default: false).
*
* @return string The API response, or an error message in JSON format.
*/
public function fetchAgent($agent_id, $force = false) {
// we need agent details for URL and JWT token
$agent = $this->getAgentDetails($agent_id);
$agentDetails = $this->getAgentIDDetails($agent_id);
// Safe exit in case the agent is not found
if (empty($agentDetails)) {
return json_encode(['error' => 'Agent not found']);
}
$agent = $agentDetails[0];
$agent_cache_name = 'agent' . $agent_id . '_cache';
$agent_cache_time = 'agent' . $agent_id . '_time';
// check if the cache is still valid, unless force-refresh is requested
if (!$force && this->checkAgentCache($agent_id)) {
if (!$force && $this->checkAgentCache($agent_id)) {
return $_SESSION[$agent_cache_name];
}
// generate the JWT token
$payload = [
'agent_id' => $agent_id,
'timestamp' => time()
];
$jwt = $this->generateAgentToken($payload, $agent['secret_key']);
// Make the API request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $agent[0]['url']);
curl_setopt($ch, CURLOPT_URL, $agent['url'] . $agent['agent_endpoint']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // timeout 10 seconds
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $jwt,
'Content-Type: application/json'
]);
$response = curl_exec($ch);
$curl_error = curl_error($ch);
$curl_errno = curl_errno($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// general curl error
if ($curl_error) {
// curl error
if ($curl_errno) {
return json_encode(['error' => 'curl error: ' . $curl_error]);
}
// response is not 200 OK
if ($http_code !== 200) {
return json_encode(['error' => 'HTTP error: ' . $http_code]);
}
// other custom error(s)
if (strpos($response, 'Auth header not received') !== false) {
return json_encode(['error' => 'Auth header not received']);
}
// Cache the result and the timestamp if the response is successful
$_SESSION[$agent_cache_name] = $response;
// We decode it so that it's pure JSON and not escaped
$_SESSION[$agent_cache_name] = json_decode($response, true);
$_SESSION[$agent_cache_time] = time();
return $response;
}
// clear agent cache
/**
* Clears the cached data for a specific agent.
*
* @param int $agent_id The agent ID for which the cache should be cleared.
*/
public function clearAgentCache($agent_id) {
$_SESSION["agent{$agent_id}_cache"] = '';
$_SESSION["agent{$agent_id}_cache_time"] = '';
}
// get latest stored jilo agents data
public function getLatestData($platform_id, $agent_type, $metric_type) {
// retrieves data already stored in db from another function (or the jilo-server to-be)
/**
* Gets a value from a nested array using dot notation
* e.g. "bridge_selector.bridge_count" will get $array['bridge_selector']['bridge_count']
*
* @param array $array The array to search in
* @param string $path The path in dot notation
* @return mixed|null The value if found, null otherwise
*/
private function getNestedValue($array, $path) {
$keys = explode('.', $path);
$value = $array;
foreach ($keys as $key) {
if (!isset($value[$key])) {
return null;
}
$value = $value[$key];
}
return $value;
}
/**
* Retrieves the latest stored data for a specific platform, agent type, and metric type.
*
* @param int $platform_id The platform ID.
* @param string $agent_type The agent type.
* @param string $metric_type The metric type to filter by.
*
* @return mixed The latest stored data.
*/
public function getLatestData($platform_id, $agent_type, $metric_type) {
$sql = 'SELECT
jac.timestamp,
jac.response_content,
jac.agent_id,
jat.description
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
WHERE
ja.platform_id = :platform_id
AND jat.description = :agent_type
AND jac.status_code = 200
ORDER BY
jac.timestamp DESC
LIMIT 1';
$query = $this->db->prepare($sql);
$query->execute([
':platform_id' => $platform_id,
':agent_type' => $agent_type
]);
$result = $query->fetch(PDO::FETCH_ASSOC);
if ($result) {
// Parse the JSON response content
$data = json_decode($result['response_content'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
// Extract the specific metric value from the response based on agent type
if ($agent_type === 'jvb') {
$value = $this->getNestedValue($data['jvb_api_data'], $metric_type);
if ($value !== null) {
return [
'value' => $value,
'timestamp' => $result['timestamp']
];
}
} elseif ($agent_type === 'jicofo') {
$value = $this->getNestedValue($data['jicofo_api_data'], $metric_type);
if ($value !== null) {
return [
'value' => $value,
'timestamp' => $result['timestamp']
];
}
} elseif ($agent_type === 'jigasi') {
$value = $this->getNestedValue($data['jigasi_api_data'], $metric_type);
if ($value !== null) {
return [
'value' => $value,
'timestamp' => $result['timestamp']
];
}
} elseif ($agent_type === 'prosody') {
$value = $this->getNestedValue($data['prosody_api_data'], $metric_type);
if ($value !== null) {
return [
'value' => $value,
'timestamp' => $result['timestamp']
];
}
} elseif ($agent_type === 'nginx') {
$value = $this->getNestedValue($data['nginx_api_data'], $metric_type);
if ($value !== null) {
return [
'value' => $value,
'timestamp' => $result['timestamp']
];
}
}
}
return null;
}
/**
* Gets historical data for a specific metric from agent checks
*
* @param int $platform_id The platform ID
* @param string $agent_type The type of agent (e.g., 'jvb', 'jicofo')
* @param string $metric_type The type of metric to retrieve
* @param string $from_time Start time in Y-m-d format
* @param string $until_time End time in Y-m-d format
* @return array Array with the dataset from agent checks
*/
public function getHistoricalData($platform_id, $agent_type, $metric_type, $from_time, $until_time) {
// Get data from agent checks
$sql = 'SELECT
DATE(jac.timestamp) as date,
jac.response_content,
COUNT(*) as checks_count
FROM
jilo_agent_checks jac
JOIN
jilo_agents ja ON jac.agent_id = ja.id
JOIN
jilo_agent_types jat ON ja.agent_type_id = jat.id
WHERE
ja.platform_id = :platform_id
AND jat.description = :agent_type
AND jac.status_code = 200
AND DATE(jac.timestamp) BETWEEN :from_time AND :until_time
GROUP BY
DATE(jac.timestamp)
ORDER BY
DATE(jac.timestamp)';
$query = $this->db->prepare($sql);
$query->execute([
':platform_id' => $platform_id,
':agent_type' => $agent_type,
':from_time' => $from_time,
':until_time' => $until_time
]);
$results = $query->fetchAll(PDO::FETCH_ASSOC);
$data = [];
foreach ($results as $row) {
$json_data = json_decode($row['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) {
$data[] = [
'date' => $row['date'],
'value' => $value
];
}
}
}
return $data;
}
}
?>

View File

@ -1,14 +1,40 @@
<?php
/**
* class Component
*
* Provides methods to interact with Jitsi component events in the database.
*/
class Component {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
/**
* Component constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
$this->db = $database->getConnection();
}
// list of component events
/**
* Retrieves Jitsi component events based on various filters.
*
* @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.
* @param int $offset The offset for pagination.
* @param int $items_per_page The number of items to retrieve per page.
*
* @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='') {
// time period drill-down
@ -63,7 +89,6 @@ ORDER BY
return $query->fetchAll(PDO::FETCH_ASSOC);
}
}
?>

View File

@ -1,14 +1,38 @@
<?php
/**
* class Conference
*
* Provides methods for querying conference-related data from the database.
*/
class Conference {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
/**
* Conference constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
$this->db = $database->getConnection();
}
// search/list specific conference ID
/**
* Retrieves conference data by conference ID within a specific time range.
*
* @param string $conference_id The conference ID.
* @param string $from_time The start date in 'YYYY-MM-DD' format.
* @param string $until_time The end date in 'YYYY-MM-DD' format.
* @param int $offset The offset for pagination.
* @param int $items_per_page The number of items to retrieve per page.
*
* @return array The list of conference events or an empty array if no results.
*/
public function conferenceById($conference_id, $from_time, $until_time, $offset=0, $items_per_page='') {
// time period drill-down
@ -83,7 +107,17 @@ ORDER BY
}
// search/list specific conference name
/**
* Retrieves conference data by conference name within a specific time range.
*
* @param string $conference_name The conference name.
* @param string $from_time The start date in 'YYYY-MM-DD' format.
* @param string $until_time The end date in 'YYYY-MM-DD' format.
* @param int $offset The offset for pagination.
* @param int $items_per_page The number of items to retrieve per page.
*
* @return array The list of conference events or an empty array if no results.
*/
public function conferenceByName($conference_name, $from_time, $until_time, $offset=0, $items_per_page='') {
// time period drill-down
@ -158,7 +192,16 @@ ORDER BY
}
// list of all conferences
/**
* Retrieves all conferences within a specific time range, formatted.
*
* @param string $from_time The start date in 'YYYY-MM-DD' format.
* @param string $until_time The end date in 'YYYY-MM-DD' format.
* @param int $offset The offset for pagination.
* @param int $items_per_page The number of items to retrieve per page.
*
* @return array The list of formatted conference data or an empty array if no results.
*/
public function conferencesAllFormatted($from_time, $until_time, $offset=0, $items_per_page='') {
// time period drill-down
@ -257,7 +300,15 @@ ORDER BY
return $query->fetchAll(PDO::FETCH_ASSOC);
}
// number of conferences
/**
* Retrieves the number of conferences within a specific time range.
*
* @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 number of conferences found.
*/
public function conferenceNumber($from_time, $until_time) {
// time period drill-down

View File

@ -1,8 +1,64 @@
<?php
/**
* class Config
*
* Handles editing and fetching configuration files.
*/
class Config {
// loading the config.js
/**
* Edits a configuration 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.
*
* @return mixed Returns true on success, or an error message on failure.
*/
public function editConfigFile($updatedConfig, $config_file) {
// first we get a fresh config file contents as text
$config_contents = file_get_contents($config_file);
if (!$config_contents) {
return "Failed to read the config file \"$config_file\".";
}
// loop through the variables and updated them
foreach ($updatedConfig as $key => $newValue) {
// we look for 'option' => value
// option is always in single quotes
// value is without quotes, because it could be true/false
$pattern = "/(['\"]{$key}['\"]\s*=>\s*)([^,]+),/";
// prepare the value, make booleans w/out single quotes
if ($newValue === 'true') {
$replacementValue = 'true';
} elseif ($newValue === 'false') {
$replacementValue = 'false';
} else {
$replacementValue = var_export($newValue, true);
}
// value replacing
$config_contents = preg_replace($pattern, "$1{$replacementValue},", $config_contents);
}
// write the new config file
if (!file_put_contents($config_file, $config_contents)) {
return "Failed to write the config file \"$config_file\".";
}
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';
@ -44,7 +100,14 @@ class Config {
}
// loading the interface_config.js
/**
* 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';
@ -85,7 +148,6 @@ class Config {
}
}
?>

View File

@ -1,10 +1,33 @@
<?php
/**
* class Database
*
* Manages database connections for SQLite and MySQL (or MariaDB).
*/
class Database {
/**
* @var PDO|null $pdo The database connection instance.
*/
private $pdo;
/**
* Database constructor.
* Initializes the database connection based on provided options.
*
* @param array $options An associative array with database connection options:
* - type: The database type ('sqlite', 'mysql', or 'mariadb').
* - dbFile: The path to the SQLite database file (required for SQLite).
* - host: The database host (required for MySQL).
* - port: The port for MySQL (optional, default: 3306).
* - dbname: The name of the MySQL database (required for MySQL).
* - user: The username for MySQL (required for MySQL).
* - password: The password for MySQL (optional).
*
* @throws Exception If required extensions are not loaded or options are invalid.
*/
public function __construct($options) {
// pdo needed
// check if PDO extension is loaded
if ( !extension_loaded('pdo') ) {
$error = getError('PDO extension not loaded.');
}
@ -14,7 +37,7 @@ class Database {
$error = getError('Database type is not set.');
}
// database type
// connect based on database type
switch ($options['type']) {
case 'sqlite':
$this->connectSqlite($options);
@ -27,6 +50,15 @@ class Database {
}
}
/**
* Establishes a connection to a SQLite database.
*
* @param array $options An associative array with SQLite connection options:
* - dbFile: The path to the SQLite database file.
*
* @throws Exception If the SQLite PDO extension is not loaded or the database file is missing.
*/
private function connectSqlite($options) {
// pdo_sqlite extension is needed
if (!extension_loaded('pdo_sqlite')) {
@ -35,7 +67,7 @@ class Database {
// SQLite options
if (empty($options['dbFile']) || !file_exists($options['dbFile'])) {
$error = getError("SQLite database file \"{$dbFile}\" not found.");
$error = getError("SQLite database file \"{$options['dbFile']}\" not found.");
}
// connect to SQLite
@ -49,6 +81,19 @@ class Database {
}
}
/**
* Establishes a connection to a MySQL (or MariaDB) database.
*
* @param array $options An associative array with MySQL connection options:
* - host: The database host.
* - port: The database port (default: 3306).
* - dbname: The name of the database.
* - user: The database username.
* - password: The database password (optional).
*
* @throws Exception If the MySQL PDO extension is not loaded or required options are missing.
*/
private function connectMysql($options) {
// pdo_mysql extension is needed
if (!extension_loaded('pdo_mysql')) {
@ -66,10 +111,16 @@ class Database {
$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(), $config['environment']);
$error = getError('MySQL connection failed: ', $e->getMessage());
}
}
/**
* Retrieves the current PDO connection instance.
*
* @return PDO|null The PDO instance or null if no connection is established.
*/
public function getConnection() {
return $this->pdo;
}

View File

@ -0,0 +1,155 @@
<?php
/**
* class Host
*
* Manages the hosts in the database, providing methods to retrieve, add, edit, and delete host entries.
*/
class Host {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
/**
* Host constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
$this->db = $database->getConnection();
}
/**
* Get details of a specified host ID (or all hosts) in a specified platform ID.
*
* @param string $platform_id The platform ID to filter the hosts by (optional).
* @param string $host_id The host ID to filter the details (optional).
*
* @return array The details of the host(s) in the form of an associative array.
*/
public function getHostDetails($platform_id = '', $host_id = '') {
$sql = 'SELECT
id,
address,
port,
platform_id,
name
FROM
hosts';
if ($platform_id !== '' && $host_id !== '') {
$sql .= ' WHERE platform_id = :platform_id AND id = :host_id';
} elseif ($platform_id !== '') {
$sql .= ' WHERE platform_id = :platform_id';
} elseif ($host_id !== '') {
$sql .= ' WHERE id = :host_id';
}
$query = $this->db->prepare($sql);
if ($platform_id !== '') {
$query->bindParam(':platform_id', $platform_id);
}
if ($host_id !== '') {
$query->bindParam(':host_id', $host_id);
}
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Add a new host to the database.
*
* @param array $newHost An associative array containing the details of the host to be added.
*
* @return bool True if the host was added successfully, otherwise false.
*/
public function addHost($newHost) {
try {
$sql = 'INSERT INTO hosts
(address, port, platform_id, name)
VALUES
(:address, :port, :platform_id, :name)';
$query = $this->db->prepare($sql);
$query->execute([
':address' => $newHost['address'],
':port' => $newHost['port'],
':platform_id' => $newHost['platform_id'],
':name' => $newHost['name'],
]);
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* Edit an existing host in the database.
*
* @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.
*/
public function editHost($platform_id, $updatedHost) {
try {
$sql = 'UPDATE hosts SET
address = :address,
port = :port,
name = :name
WHERE
id = :id';
$query = $this->db->prepare($sql);
$query->execute([
':id' => $updatedHost['id'],
':address' => $updatedHost['address'],
':port' => $updatedHost['port'],
':name' => $updatedHost['name'],
]);
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* Delete a host from the database.
*
* @param int $host_id The ID of the host to be deleted.
*
* @return bool True if the host was deleted successfully, otherwise false.
*/
public function deleteHost($host_id) {
try {
$sql = 'DELETE FROM hosts
WHERE
id = :host_id';
$query = $this->db->prepare($sql);
$query->bindParam(':host_id', $host_id);
$query->execute();
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
}
?>

View File

@ -1,13 +1,36 @@
<?php
/**
* class Log
*
* Handles logging events into a database and reading log entries.
*/
class Log {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
/**
* Logs constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
$this->db = $database->getConnection();
}
// insert log event
/**
* Insert a log event into the database.
*
* @param int $user_id The ID of the user associated with the log event.
* @param string $message The log message to insert.
* @param string $scope The scope of the log event (e.g., 'user', 'system'). Default is 'user'.
*
* @return bool|string True on success, or an error message on failure.
*/
public function insertLog($user_id, $message, $scope='user') {
try {
$sql = 'INSERT INTO logs
@ -29,7 +52,17 @@ class Log {
}
}
// read logs
/**
* Retrieve log entries from the database.
*
* @param int $user_id The ID of the user whose logs are being retrieved.
* @param string $scope The scope of the logs ('user' or 'system').
* @param int $offset The offset for pagination. Default is 0.
* @param int $items_per_page The number of log entries to retrieve per page. Default is no limit.
*
* @return array An array of log entries.
*/
public function readLog($user_id, $scope, $offset=0, $items_per_page='') {
if ($scope === 'user') {
$sql = 'SELECT * FROM logs WHERE user_id = :user_id ORDER BY time DESC';

View File

@ -0,0 +1,178 @@
<?php
class Messages {
// Message types
const TYPE_SUCCESS = 'success';
const TYPE_ERROR = 'danger';
const TYPE_INFO = 'info';
const TYPE_WARNING = 'warning';
// Default message configurations
const NOTICE = [
'DEFAULT' => [
'type' => self::TYPE_INFO,
'dismissible' => true
]
];
const ERROR = [
'DEFAULT' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
]
];
const LOGIN = [
'LOGIN_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'LOGIN_FAILED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'LOGOUT_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'IP_BLACKLISTED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'IP_NOT_WHITELISTED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'TOO_MANY_ATTEMPTS' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
]
];
const SECURITY = [
'WHITELIST_ADD_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'WHITELIST_ADD_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'WHITELIST_REMOVE_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'WHITELIST_REMOVE_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'BLACKLIST_ADD_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'BLACKLIST_ADD_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'BLACKLIST_REMOVE_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'BLACKLIST_REMOVE_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'RATE_LIMIT_INFO' => [
'type' => self::TYPE_INFO,
'dismissible' => false
],
'PERMISSION_DENIED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'IP_REQUIRED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
]
];
private static $strings = null;
/**
* Get message strings
*/
private static function getStrings() {
if (self::$strings === null) {
self::$strings = require __DIR__ . '/../includes/messages-strings.php';
}
return self::$strings;
}
/**
* Get message configuration by key
*/
public static function get($category, $key) {
$config = constant("self::$category")[$key] ?? null;
if (!$config) return null;
$strings = self::getStrings();
$message = $strings[$category][$key] ?? '';
return array_merge($config, ['message' => $message]);
}
/**
* Render message HTML
*/
// Usage: echo Messages::render('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
public static function render($category, $key, $customMessage = null, $dismissible = null, $small = false, $sanitize = true) {
$config = self::get($category, $key);
if (!$config) return '';
$message = $customMessage ?? $config['message'];
$isDismissible = $dismissible ?? $config['dismissible'] ?? false;
$dismissClass = $isDismissible ? ' alert-dismissible fade show' : '';
$dismissButton = $isDismissible ? '<button type="button" class="btn-close' . ($small ? ' btn-close-sm' : '') . '" data-bs-dismiss="alert" aria-label="Close"></button>' : '';
$smallClass = $small ? ' alert-sm' : '';
return sprintf(
'<div class="alert alert-%s%s%s" role="alert">%s%s</div>',
$config['type'],
$dismissClass,
$smallClass,
$sanitize ? htmlspecialchars($message) : $message,
$dismissButton
);
}
/**
* Store message in session for display after redirect
*/
// Usage: Messages::flash('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
public static function flash($category, $key, $customMessage = null, $dismissible = null, $small = false) {
if (!isset($_SESSION['flash_messages'])) {
$_SESSION['flash_messages'] = [];
}
// Get the message configuration
$config = self::get($category, $key);
$isDismissible = $dismissible ?? $config['dismissible'] ?? false;
$_SESSION['flash_messages'][] = [
'category' => $category,
'key' => $key,
'custom_message' => $customMessage,
'dismissible' => $isDismissible,
'small' => $small
];
}
/**
* Get and clear all flash messages
*/
public static function getFlash() {
$messages = $_SESSION['flash_messages'] ?? [];
unset($_SESSION['flash_messages']);
return $messages;
}
}

View File

@ -1,14 +1,39 @@
<?php
/**
* class Participant
*
* This class provides methods to retrieve information about participants and their related conference data.
* It supports querying participant details by ID, name, or IP, as well as listing all participants and counting them within a specific time frame.
*/
class Participant {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
/**
* Constructor
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
$this->db = $database->getConnection();
}
// search/list specific participant ID
/**
* Retrieve conferences by participant ID within a specified time period.
*
* @param string $participant_id The participant's ID (endpoint_id).
* @param string $from_time The start date (format: 'YYYY-MM-DD'). Defaults to '0000-01-01' if empty.
* @param string $until_time The end date (format: 'YYYY-MM-DD'). Defaults to '9999-12-31' if empty.
* @param int $offset The offset for pagination.
* @param int $items_per_page The number of items per page for pagination.
*
* @return array List of conferences involving the specified participant ID.
*/
public function conferenceByParticipantId($participant_id, $from_time, $until_time, $offset=0, $items_per_page='') {
// time period drill-down
@ -83,7 +108,17 @@ ORDER BY
}
// search/list specific participant name (stats_id)
/**
* Retrieve conferences by participant name within a specified time period.
*
* @param string $participant_name The participant's name (stats_id).
* @param string $from_time The start date (format: 'YYYY-MM-DD'). Defaults to '0000-01-01' if empty.
* @param string $until_time The end date (format: 'YYYY-MM-DD'). Defaults to '9999-12-31' if empty.
* @param int $offset The offset for pagination.
* @param int $items_per_page The number of items per page for pagination.
*
* @return array List of conferences involving the specified participant name.
*/
public function conferenceByParticipantName($participant_name, $from_time, $until_time, $offset=0, $items_per_page='') {
// time period drill-down
@ -158,7 +193,17 @@ ORDER BY
}
// search/list specific participant IP
/**
* Retrieve conferences by participant IP within a specified time period.
*
* @param string $participant_ip The participant's IP address.
* @param string $from_time The start date (format: 'YYYY-MM-DD'). Defaults to '0000-01-01' if empty.
* @param string $until_time The end date (format: 'YYYY-MM-DD'). Defaults to '9999-12-31' if empty.
* @param int $offset The offset for pagination.
* @param int $items_per_page The number of items per page for pagination.
*
* @return array List of conferences involving the specified participant IP.
*/
public function conferenceByParticipantIP($participant_ip, $from_time, $until_time, $offset=0, $items_per_page='') {
// time period drill-down
@ -233,7 +278,16 @@ ORDER BY
}
// list of all participants
/**
* Retrieve a list of all participants within a specified time period.
*
* @param string $from_time The start date (format: 'YYYY-MM-DD'). Defaults to '0000-01-01' if empty.
* @param string $until_time The end date (format: 'YYYY-MM-DD'). Defaults to '9999-12-31' if empty.
* @param int $offset The offset for pagination.
* @param int $items_per_page The number of items per page for pagination.
*
* @return array List of all participants.
*/
public function participantsAll($from_time, $until_time, $offset=0, $items_per_page='') {
// time period drill-down
@ -274,7 +328,15 @@ ORDER BY p.id";
return $query->fetchAll(PDO::FETCH_ASSOC);
}
// number of participants
/**
* Count the number of participants within a specified time period.
*
* @param string $from_time The start date (format: 'YYYY-MM-DD'). Defaults to '0000-01-01' if empty.
* @param string $until_time The end date (format: 'YYYY-MM-DD'). Defaults to '9999-12-31' if empty.
*
* @return int The number of participants.
*/
public function participantNumber($from_time, $until_time) {
// time period drill-down
@ -309,7 +371,6 @@ AND pe.event_type = 'participant joining'";
return $query->fetchAll(PDO::FETCH_ASSOC);
}
}
?>

View File

@ -1,13 +1,34 @@
<?php
/**
* class Platform
*
* Handles platform management in the database, including retrieving, adding, editing, and deleting platforms.
*/
class Platform {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
/**
* Platform constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
$this->db = $database->getConnection();
}
// get details of a specified platform ID (or all)
/**
* Retrieve details of a specific platform or all platforms.
*
* @param string $platform_id The ID of the platform to retrieve details for (optional).
*
* @return array An associative array containing platform details.
*/
public function getPlatformDetails($platform_id = '') {
$sql = 'SELECT * FROM platforms';
if ($platform_id !== '') {
@ -23,7 +44,17 @@ class Platform {
return $query->fetchAll(PDO::FETCH_ASSOC);
}
// add new platform
/**
* Add a new platform to the database.
*
* @param array $newPlatform An associative array containing the details of the new platform:
* - `name` (string): The name of the platform.
* - `jitsi_url` (string): The URL for the Jitsi integration.
* - `jilo_database` (string): The database name for Jilo integration.
*
* @return bool|string True if the platform was added successfully, or an error message on failure.
*/
public function addPlatform($newPlatform) {
try {
$sql = 'INSERT INTO platforms
@ -45,7 +76,18 @@ class Platform {
}
}
// edit an existing platform
/**
* Edit an existing platform in the database.
*
* @param int $platform_id The ID of the platform to update.
* @param array $updatedPlatform An associative array containing the updated platform details:
* - `name` (string): The updated name of the platform.
* - `jitsi_url` (string): The updated Jitsi URL.
* - `jilo_database` (string): The updated Jilo database name.
*
* @return bool|string True if the platform was updated successfully, or an error message on failure.
*/
public function editPlatform($platform_id, $updatedPlatform) {
try {
$sql = 'UPDATE platforms SET
@ -70,7 +112,14 @@ class Platform {
}
}
// delete a platform
/**
* Delete a platform from the database.
*
* @param int $platform_id The ID of the platform to delete.
*
* @return bool|string True if the platform was deleted successfully, or an error message on failure.
*/
public function deletePlatform($platform_id) {
try {
$sql = 'DELETE FROM platforms
@ -88,7 +137,6 @@ class Platform {
}
}
}
?>

View File

@ -0,0 +1,457 @@
<?php
class RateLimiter {
public $db;
private $log;
public $maxAttempts = 5; // Maximum login attempts
public $decayMinutes = 15; // Time window in minutes
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
public $autoBlacklistDuration = 24; // Hours to blacklist for
public $ratelimitTable = 'login_attempts';
public $whitelistTable = 'ip_whitelist';
public $blacklistTable = 'ip_blacklist';
public function __construct($database) {
$this->db = $database->getConnection();
$this->log = new Log($database);
$this->createTablesIfNotExist();
}
// Database preparation
private function createTablesIfNotExist() {
// Login attempts table
$sql = "CREATE TABLE IF NOT EXISTS {$this->ratelimitTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
attempted_at TEXT DEFAULT (DATETIME('now'))
)";
$this->db->exec($sql);
// IP whitelist table
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description TEXT,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
)";
$this->db->exec($sql);
// IP blacklist table
$sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT,
expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
)";
$this->db->exec($sql);
// Default IPs to whitelist (local interface and private networks IPs)
$defaultIps = [
['127.0.0.1', false, 'localhost IPv4'],
['::1', false, 'localhost IPv6'],
['10.0.0.0/8', true, 'Private network (Class A)'],
['172.16.0.0/12', true, 'Private network (Class B)'],
['192.168.0.0/16', true, 'Private network (Class C)']
];
// Insert default whitelisted IPs if they don't exist
$stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->whitelistTable}
(ip_address, is_network, description, created_by)
VALUES (?, ?, ?, 'system')");
foreach ($defaultIps as $ip) {
$stmt->execute([$ip[0], $ip[1], $ip[2]]);
}
// Insert known malicious networks
$defaultBlacklist = [
['0.0.0.0/8', true, 'Reserved address space - RFC 1122'],
['100.64.0.0/10', true, 'Carrier-grade NAT space - RFC 6598'],
['192.0.2.0/24', true, 'TEST-NET-1 Documentation space - RFC 5737'],
['198.51.100.0/24', true, 'TEST-NET-2 Documentation space - RFC 5737'],
['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737']
];
$stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->blacklistTable}
(ip_address, is_network, reason, created_by)
VALUES (?, ?, ?, 'system')");
foreach ($defaultBlacklist as $ip) {
$stmt->execute([$ip[0], $ip[1], $ip[2]]);
}
}
/**
* Get number of recent login attempts for an IP
*/
public function getRecentAttempts($ip) {
$stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->ratelimitTable}
WHERE ip_address = ? AND attempted_at > datetime('now', '-' || :minutes || ' minutes')");
$stmt->execute([$ip, $this->decayMinutes]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return intval($result['attempts']);
}
/**
* Check if an IP is blacklisted
*/
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->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// Skip expired entries
if ($row['expiry_time'] !== null && strtotime($row['expiry_time']) < time()) {
continue;
}
if ($row['is_network']) {
if ($this->ipInRange($ip, $row['ip_address'])) {
return true;
}
} else {
if ($ip === $row['ip_address']) {
return true;
}
}
}
return false;
}
/**
* 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();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
if ($row['is_network']) {
if ($this->ipInRange($ip, $row['ip_address'])) {
return true;
}
} else {
if ($ip === $row['ip_address']) {
return true;
}
}
}
return false;
}
private function ipInRange($ip, $cidr) {
list($subnet, $bits) = explode('/', $cidr);
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$subnet &= $mask;
return ($ip & $mask) == $subnet;
}
// Add to whitelist
public function addToWhitelist($ip, $isNetwork = false, $description = '', $createdBy = 'system', $userId = null) {
try {
// Check if IP is blacklisted first
if ($this->isIpBlacklisted($ip)) {
$message = "Cannot whitelist {$ip} - IP is currently blacklisted";
if ($userId) {
$this->log->insertLog($userId, "IP Whitelist: {$message}", 'system');
Messages::flash('ERROR', 'DEFAULT', $message);
}
return false;
}
$stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->whitelistTable}
(ip_address, is_network, description, created_by)
VALUES (?, ?, ?, ?)");
$result = $stmt->execute([$ip, $isNetwork, $description, $createdBy]);
if ($result) {
$logMessage = sprintf(
'IP Whitelist: Added %s "%s" by %s. Description: %s',
$isNetwork ? 'network' : 'IP',
$ip,
$createdBy,
$description
);
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
}
return $result;
} 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());
}
return false;
}
}
// Remove from whitelist
public function removeFromWhitelist($ip, $removedBy = 'system', $userId = null) {
try {
// Get IP details before removal for logging
$stmt = $this->db->prepare("SELECT * FROM {$this->whitelistTable} WHERE ip_address = ?");
$stmt->execute([$ip]);
$ipDetails = $stmt->fetch(PDO::FETCH_ASSOC);
// Remove the IP
$stmt = $this->db->prepare("DELETE FROM {$this->whitelistTable} WHERE ip_address = ?");
$result = $stmt->execute([$ip]);
if ($result && $ipDetails) {
$logMessage = sprintf(
'IP Whitelist: Removed %s "%s" by %s. Was added by: %s',
$ipDetails['is_network'] ? 'network' : 'IP',
$ip,
$removedBy,
$ipDetails['created_by']
);
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
}
return $result;
} 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());
}
return false;
}
}
public function addToBlacklist($ip, $isNetwork = false, $reason = '', $createdBy = 'system', $userId = null, $expiryHours = null) {
try {
// Check if IP is whitelisted first
if ($this->isIpWhitelisted($ip)) {
$message = "Cannot blacklist {$ip} - IP is currently whitelisted";
if ($userId) {
$this->log->insertLog($userId, "IP Blacklist: {$message}", 'system');
Messages::flash('ERROR', 'DEFAULT', $message);
}
return false;
}
$expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null;
$stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->blacklistTable}
(ip_address, is_network, reason, expiry_time, created_by)
VALUES (?, ?, ?, ?, ?)");
$result = $stmt->execute([$ip, $isNetwork, $reason, $expiryTime, $createdBy]);
if ($result) {
$logMessage = sprintf(
'IP Blacklist: Added %s "%s" by %s. Reason: %s. Expires: %s',
$isNetwork ? 'network' : 'IP',
$ip,
$createdBy,
$reason,
$expiryTime ?? 'never'
);
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
}
return $result;
} 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());
}
return false;
}
}
public function removeFromBlacklist($ip, $removedBy = 'system', $userId = null) {
try {
// Get IP details before removal for logging
$stmt = $this->db->prepare("SELECT * FROM {$this->blacklistTable} WHERE ip_address = ?");
$stmt->execute([$ip]);
$ipDetails = $stmt->fetch(PDO::FETCH_ASSOC);
// Remove the IP
$stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable} WHERE ip_address = ?");
$result = $stmt->execute([$ip]);
if ($result && $ipDetails) {
$logMessage = sprintf(
'IP Blacklist: Removed %s "%s" by %s. Was added by: %s. Reason was: %s',
$ipDetails['is_network'] ? 'network' : 'IP',
$ip,
$removedBy,
$ipDetails['created_by'],
$ipDetails['reason']
);
$this->log->insertLog($userId ?? 0, $logMessage, 'system');
}
return $result;
} 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());
}
return false;
}
}
public function getWhitelistedIps() {
$stmt = $this->db->prepare("SELECT * FROM {$this->whitelistTable} ORDER BY created_at DESC");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getBlacklistedIps() {
$stmt = $this->db->prepare("SELECT * FROM {$this->blacklistTable} ORDER BY created_at DESC");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function cleanupExpiredEntries() {
try {
// Remove expired blacklist entries
$stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable}
WHERE expiry_time IS NOT NULL AND expiry_time < datetime('now')");
$stmt->execute();
// Clean old login attempts
$stmt = $this->db->prepare("DELETE FROM {$this->ratelimitTable}
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')");
$stmt->execute([':minutes' => $this->decayMinutes]);
return true;
} catch (Exception $e) {
$this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system');
Messages::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage());
return false;
}
}
public function isAllowed($username, $ipAddress) {
// 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 attempts
$this->clearOldAttempts();
// Check if we've hit the rate limit
if ($this->tooManyAttempts($username, $ipAddress)) {
return false;
}
// Check total attempts across all usernames from this IP
$sql = "SELECT COUNT(*) as total_attempts
FROM {$this->ratelimitTable}
WHERE ip_address = :ip
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':minutes' => $this->decayMinutes
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
// Check if we would hit auto-blacklist threshold
return $result['total_attempts'] < $this->autoBlacklistThreshold;
}
public function attempt($username, $ipAddress) {
// Record this attempt
$sql = "INSERT INTO {$this->ratelimitTable} (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
);
return false;
}
return true;
}
public function tooManyAttempts($username, $ipAddress) {
$sql = "SELECT COUNT(*) as attempts
FROM {$this->ratelimitTable}
WHERE ip_address = :ip
AND username = :username
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':username' => $username,
':minutes' => $this->decayMinutes
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result['attempts'] >= $this->maxAttempts;
}
public function clearOldAttempts() {
$sql = "DELETE FROM {$this->ratelimitTable}
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':minutes' => $this->decayMinutes
]);
}
public function getRemainingAttempts($username, $ipAddress) {
$sql = "SELECT COUNT(*) as attempts
FROM {$this->ratelimitTable}
WHERE ip_address = :ip
AND username = :username
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':username' => $username,
':minutes' => $this->decayMinutes
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return max(0, $this->maxAttempts - $result['attempts']);
}
public function getDecayMinutes() {
return $this->decayMinutes;
}
}

View File

@ -1,20 +1,46 @@
<?php
/**
* class Router
*
* A simple Router class to manage URL-to-callback mapping and dispatch requests to the appropriate controllers and methods.
* The class supports defining routes, matching URLs against patterns, and invoking callbacks for matched routes.
*/
class Router {
/**
* @var array $routes Associative array of route patterns and their corresponding callbacks.
*/
private $routes = [];
public function add() {
/**
* Adds a new route to the router.
*
* @param string $pattern The URL pattern to match (regular expression).
* @param string $callback The callback for the route in the format "Controller@Method".
*
* @return void
*/
public function add($pattern, $callback) {
$this->routes[$pattern] = $callback;
}
/**
* Dispatches a request to the appropriate route callback.
*
* @param string $url The URL to match against the defined routes.
*
* @return void Outputs the result of the invoked callback or a 404 error if no route matches.
*/
public function dispatch($url) {
// remove variables from url
// remove query string variables from url
$url = strtok($url, '?');
foreach ($this->routes as $pattern => $callback) {
// check if the URL matches the current route pattern
if (preg_match('#^' . $pattern . '$#', $url, $matches)) {
// move any exact match
// remove the exact match to extrat parameters
array_shift($matches);
return $this->invoke($callback, $matches);
}
@ -25,11 +51,34 @@ class Router {
echo '404 page not found';
}
/**
* Invokes the callback for a matched route.
*
* @param string $callback The callback for the route in the format "Controller@Method".
* @param array $params Parameters extracted from the route pattern.
*
* @return void Executes the specified method on the specified controller with the provided parameters.
*
* @throws Exception If the controller class or method does not exist.
*/
private function invoke($callback, $params) {
list($controllerName, $methodName) = explode('@', $callback);
// $controllerClass = "\\App\\Controllers\\$controllerName";
$controllerClass = "../pages/$pageName";
$controllerClass = "../pages/$controllerName";
// ensure the controller class exists
if (!class_exists($controllerClass)) {
throw new Exception("Controller '$controllerClass' not found.");
}
$controller = new $controllerClass();
// ensure the method exists on the controller
if (!method_exists($controller, $methodName)) {
throw new Exception("Method '$methodName' not found in controller '$controllerClass'.");
}
// call the controller's method with the parameters
call_user_func_array([$controller, $methodName], $params);
}

View File

@ -0,0 +1,56 @@
<?php
/**
* class Server
*
* Handles server-related operations, including retrieving server status.
*/
class Server {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
/**
* Server constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
$this->db = $database->getConnection();
}
/**
* Checks the status of a Jilo server by sending a GET request to its health endpoint.
*
* @param string $host The server hostname or IP address (default: '127.0.0.1').
* @param int $port The port on which the server is running (default: 8080).
* @param string $endpoint The health check endpoint path (default: '/health').
*
* @return bool True if the server returns a 200 OK status, otherwise false.
*/
public function getServerStatus($host = '127.0.0.1', $port = 8080, $endpoint = '/health') {
$url = "http://$host:$port$endpoint";
$options = [
'http' => [
'method' => 'GET',
'timeout' => 3,
],
];
$context = stream_context_create($options);
$response = @file_get_contents($url, false, $context);
// We check the response if it's 200 OK
if ($response !== false && isset($http_response_header) && strpos($http_response_header[0], '200 OK') !== false) {
return true;
}
// If it's not 200 OK
return false;
}
}
?>

View File

@ -1,13 +1,37 @@
<?php
/**
* class User
*
* Handles user-related functionalities such as registration, login, rights management, and profile updates.
*/
class User {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
private $rateLimiter;
/**
* User constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
$this->db = $database->getConnection();
$this->rateLimiter = new RateLimiter($database);
}
// registration
/**
* Registers a new user.
*
* @param string $username The username of the new user.
* @param string $password The password for the new user.
*
* @return bool|string True if registration is successful, error message otherwise.
*/
public function register($username, $password) {
try {
// we have two inserts, start a transaction
@ -56,23 +80,53 @@ class User {
}
}
// login
/**
* Logs in a user by verifying credentials.
*
* @param string $username The username of the user.
* @param string $password The password of the user.
*
* @return bool True if login is successful, false otherwise.
*/
public function login($username, $password) {
$query = $this->db->prepare("SELECT * FROM users WHERE username = :username");
// get client IP address
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
// Record attempt
$this->rateLimiter->attempt($username, $ipAddress);
// Check rate limiting first
if (!$this->rateLimiter->isAllowed($username, $ipAddress)) {
$remainingTime = $this->rateLimiter->getDecayMinutes();
throw new Exception("Too many login attempts. Please try again in {$remainingTime} minutes.");
}
// Then check credentials
$query = $this->db->prepare("SELECT * FROM users WHERE username = :username");
$query->bindParam(':username', $username);
$query->execute();
$user = $query->fetch(PDO::FETCH_ASSOC);
if ( $user && password_verify($password, $user['password'])) {
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
return true;
} else {
return false;
}
// Get remaining attempts AFTER this failed attempt
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining.");
}
// get user ID from username
/**
* Retrieves a user ID based on the username.
*
* @param string $username The username to look up.
*
* @return array|null User ID details or null if not found.
*/
// FIXME not used now?
public function getUserId($username) {
$sql = 'SELECT id FROM users WHERE username = :username';
@ -85,7 +139,14 @@ class User {
}
// get user details
/**
* Fetches user details by user ID.
*
* @param int $user_id The user ID.
*
* @return array|null User details or null if not found.
*/
public function getUserDetails($user_id) {
$sql = 'SELECT
um.*,
@ -106,7 +167,15 @@ class User {
}
// add user right
/**
* Grants a user a specific right.
*
* @param int $user_id The user ID.
* @param int $right_id The right ID to grant.
*
* @return void
*/
public function addUserRight($user_id, $right_id) {
$sql = 'INSERT INTO users_rights
(user_id, right_id)
@ -119,7 +188,15 @@ class User {
]);
}
// remove user right
/**
* Revokes a specific right from a user.
*
* @param int $user_id The user ID.
* @param int $right_id The right ID to revoke.
*
* @return void
*/
public function removeUserRight($user_id, $right_id) {
$sql = 'DELETE FROM users_rights
WHERE
@ -133,7 +210,12 @@ class User {
]);
}
// get all rights
/**
* Retrieves all rights in the system.
*
* @return array List of rights.
*/
public function getAllRights() {
$sql = 'SELECT
id AS right_id,
@ -147,7 +229,14 @@ class User {
}
// get user rights
/**
* Retrieves the rights assigned to a specific user.
*
* @param int $user_id The user ID.
*
* @return array List of user rights.
*/
public function getUserRights($user_id) {
$sql = 'SELECT
u.id AS user_id,
@ -203,7 +292,15 @@ class User {
}
// check if the user has a specific right
/**
* Check if the user has a specific right.
*
* @param int $user_id The user ID.
* @param string $right_name The human-readable name of the user right.
*
* @return bool True if the user has the right, false otherwise.
*/
function hasRight($user_id, $right_name) {
$userRights = $this->getUserRights($user_id);
$userHasRight = false;
@ -224,7 +321,19 @@ class User {
}
// update an existing user
/**
* Updates a user's metadata in the database.
*
* @param int $user_id The ID of the user to update.
* @param array $updatedUser An associative array containing updated user data:
* - 'name' (string): The updated name of the user.
* - 'email' (string): The updated email of the user.
* - 'timezone' (string): The updated timezone of the user.
* - 'bio' (string): The updated biography of the user.
*
* @return bool|string Returns true if the update is successful, or an error message if an exception occurs.
*/
public function editUser($user_id, $updatedUser) {
try {
$sql = 'UPDATE users_meta SET
@ -250,7 +359,15 @@ class User {
}
// remove an avatar
/**
* Removes a user's avatar from the database and deletes the associated file.
*
* @param int $user_id The ID of the user whose avatar is being removed.
* @param string $old_avatar Optional. The file path of the current avatar to delete. Default is an empty string.
*
* @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs.
*/
public function removeAvatar($user_id, $old_avatar = '') {
try {
// remove from database
@ -275,7 +392,17 @@ class User {
}
// change an avatar
/**
* Updates a user's avatar by uploading a new file and saving its path in the database.
*
* @param int $user_id The ID of the user whose avatar is being updated.
* @param array $avatar_file The uploaded avatar file from the $_FILES array.
* Should include 'tmp_name', 'name', 'error', etc.
* @param string $avatars_path The directory path where avatar files should be saved.
*
* @return bool|string Returns true if the avatar is successfully updated, or an error message if an exception occurs.
*/
public function changeAvatar($user_id, $avatar_file, $avatars_path) {
try {
// check if the file was uploaded

View File

@ -31,7 +31,7 @@ return [
// default avatar
'default_avatar' => 'static/default_avatar.png',
// system info
'version' => '0.2.1',
'version' => '0.3',
// development has verbose error messages, production has not
'environment' => 'development',

View File

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

View File

@ -5,40 +5,30 @@
<script>
var ctx = document.getElementById('graph_<?= $data['graph_name'] ?>').getContext('2d');
var chartData0 = <?php echo json_encode($data['data0']); ?>;
var chartData1 = <?php echo json_encode($data['data1']); ?>;
var timeRangeName = '';
var labels = chartData0.map(function(item) {
return item.date;
});
var values0 = chartData0.map(function(item) {
return item.value;
});
var values1 = chartData1.map(function(item) {
return item.value;
});
// Prepare datasets
var datasets = [];
<?php foreach ($data['datasets'] as $dataset): ?>
var chartData = <?php echo json_encode($dataset['data']); ?>;
datasets.push({
label: '<?= $dataset['label'] ?>',
data: chartData.map(function(item) {
return {
x: item.date,
y: item.value
};
}),
borderColor: '<?= $dataset['color'] ?>',
borderWidth: 1,
fill: false
});
<?php endforeach; ?>
var graph_<?= $data['graph_name'] ?> = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: '<?= $data['graph_data0_label'] ?? '' ?>',
data: values0,
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1,
fill: false
},
{
label: '<?= $data['graph_data1_label'] ?? '' ?>',
data: values1,
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1,
fill: false
}
]
datasets: datasets
},
options: {
layout: {
@ -64,7 +54,6 @@ var graph_<?= $data['graph_name'] ?> = new Chart(ctx, {
mode: 'x'
},
zoom: {
// enabled: true,
mode: 'x',
drag: {
enabled: true, // Enable drag to select range
@ -99,29 +88,25 @@ var graph_<?= $data['graph_name'] ?> = new Chart(ctx, {
}
});
// Store the graphs in an array
// Store graph instance and title for later reference
graphs.push({
graph: graph_<?= $data['graph_name'] ?>,
label: document.getElementById('current-period-<?= $data['graph_name'] ?>')
label: '<?= $data['graph_title'] ?>'
});
// Update the time range label
function updatePeriodLabel(chart, labelElement) {
var startDate = chart.scales.x.min;
var endDate = chart.scales.x.max;
if (timeRangeName == 'today') {
labelElement.innerHTML = 'Currently displaying: ' + timeRangeName + ' (' + new Date(startDate).toLocaleDateString() + ')';
// Function to update the period label
function updatePeriodLabel(chart, label) {
var startDate = new Date(chart.scales.x.min);
var endDate = new Date(chart.scales.x.max);
var periodLabel = document.getElementById('current-period-<?= $data['graph_name'] ?>');
if (timeRangeName) {
periodLabel.textContent = label + ' (' + timeRangeName + ')';
} else {
labelElement.innerHTML = 'Currently displaying: ' + timeRangeName + ' (' + new Date(startDate).toLocaleDateString() + ' - ' + new Date(endDate).toLocaleDateString() + ')';
periodLabel.textContent = label + ' (from ' + startDate.toLocaleDateString() + ' to ' + endDate.toLocaleDateString() + ')';
}
}
// Attach the update function to the 'zoom' event
graph_<?= $data['graph_name'] ?>.options.plugins.zoom.onZoom = function({ chart }) {
updatePeriodLabel(chart, document.getElementById('current-period-<?= $data['graph_name'] ?>'));
};
// Update the label initially when the chart is rendered
updatePeriodLabel(graph_<?= $data['graph_name'] ?>, document.getElementById('current-period-<?= $data['graph_name'] ?>'));
// Initial label update
updatePeriodLabel(graph_<?= $data['graph_name'] ?>, '<?= $data['graph_title'] ?>');
</script>

View File

@ -1,9 +1,7 @@
<?php
// render config variables array
function renderConfig($configPart, $indent, $platform=false, $parent='') {
global $app_root;
global $config;
function renderConfig($configPart, $indent) {
?>
<div style="padding-left: <?= $indent ?>px; padding-bottom: 20px;">
<?php foreach ($configPart as $config_item => $config_value) { ?>
@ -15,20 +13,62 @@ function renderConfig($configPart, $indent, $platform=false, $parent='') {
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);
}
renderConfig($config_value, $indent);
$indent = 0;
} else {
// if it's not array, just display it
if ($config_item === 'registration_enabled') { ?>
<div class="border col-md-8 text-start">
<?= ($config_value === 1 || $config_value === true) ? 'true' : 'false' ?>
</div>
<?php } else { ?>
<div class="border col-md-8 text-start">
<?= htmlspecialchars($config_value ?? '')?>
</div>
<?php } ?>
<?php } ?>
</div>
<?php } ?>
</div>
<?php
}
// render config variables array
function editConfig($configPart, $indent) {
?>
<div style="padding-left: <?= $indent ?>px; padding-bottom: 20px;">
<?php foreach ($configPart as $config_item => $config_value) { ?>
<div class="row mb-1" style="padding-left: <?= $indent ?>px;">
<div class="col-md-4 text-end">
<label for="<?= htmlspecialchars($config_item) ?>" class="form-label"><?= htmlspecialchars($config_item) ?></label>
</div>
<?php
if (is_array($config_value)) {
// here we render recursively nested arrays
$indent = $indent + 50;
editConfig($config_value, $indent);
$indent = 0;
} else {
// if it's not array, just display it
?>
<div class="border col-md-8 text-start">
<?= htmlspecialchars($config_value ?? '')?>
<div class="col-md-8 text-start">
<?php if ($config_item === 'registration_enabled') { ?>
<input type="hidden" name="<?= htmlspecialchars($config_item) ?>" value="false" />
<input class="form-check-input" type="checkbox" role="switch" name="<?= htmlspecialchars($config_item) ?>" value="true" <?= ($config_value === 1 || $config_value === true) ? 'checked' : '' ?> />
<?php } elseif ($config_item === 'environment') { ?>
<select class="form-control" type="text" name="<?= htmlspecialchars($config_item) ?>">
<option value="development"<?= ($config_value === 'development') ? ' selected' : '' ?>>development</option>
<option value="production"<?= ($config_value === 'production') ? ' selected' : '' ?>>production</option>
</select>
<?php } elseif ($config_item === 'version') {?>
<input class="form-control" type="text" name="<?= htmlspecialchars($config_item) ?>" value="<?= htmlspecialchars($config_value ?? '') ?>" disabled />
<?php } elseif ($config_item === 'db_type') {?>
<input class="form-control" type="text" name="<?= htmlspecialchars($config_item) ?>" value="<?= htmlspecialchars($config_value ?? '') ?>" disabled />
<?php } else { ?>
<input class="form-control" type="text" name="<?= htmlspecialchars($config_item) ?>" value="<?= htmlspecialchars($config_value ?? '') ?>" />
<?php } ?>
</div>
<?php } ?>
</div>
@ -36,4 +76,6 @@ function renderConfig($configPart, $indent, $platform=false, $parent='') {
</div>
<?php
}
?>

View File

@ -13,10 +13,9 @@ function connectDB($config, $database = '', $dbFile = '', $platformId = '') {
'type' => 'sqlite',
'dbFile' => $dbFile,
]);
return ['db' => $db, 'error' => null];
} catch (Exception $e) {
$error = getError('Error connecting to DB.', $e->getMessage());
include '../app/templates/block-message.php';
exit();
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
}
// connecting to a jilo-web database of the web app
@ -30,10 +29,9 @@ function connectDB($config, $database = '', $dbFile = '', $platformId = '') {
'dbFile' => $config['db']['sqlite_file'],
]);
$pdo = $db->getConnection();
return ['db' => $db, 'error' => null];
} catch (Exception $e) {
$error = getError('Error connecting to DB.', $e->getMessage());
include '../app/templates/block-message.php';
exit();
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
}
// mysql/mariadb database
} elseif ($config['db']['db_type'] === 'mysql' || $config['db']['db_type'] === 'mariadb') {
@ -47,20 +45,18 @@ function connectDB($config, $database = '', $dbFile = '', $platformId = '') {
'password' => $config['db']['sql_password'],
]);
$pdo = $db->getConnection();
return ['db' => $db, 'error' => null];
} catch (Exception $e) {
$error = getError('Error connecting to DB.', $e->getMessage());
include '../app/templates/block-message.php';
exit();
return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())];
}
// unknown database
} else {
$error = "Error: unknow database type \"{$config['db']['db_type']}\"";
include '../app/templates/block-message.php';
Messages::flash('ERROR', 'DEFAULT', $error);
exit();
}
}
return $db;
}
?>

View File

@ -0,0 +1,42 @@
<?php
/**
* Generate an error or notice message based on the environment.
*
* In a production environment, hides detailed error messages and returns
* a generic message. In other environments, returns the provided message.
*
* @param string $message A user-friendly message to display.
* @param string $error The detailed error message for debugging (optional).
* @param string|null $environment The environment type ('production', 'development', etc.). If null, defaults to the configured environment.
*
* @return string The appropriate message based on the environment.
*/
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;
}
}
/**
* Render a message if it exists, and optionally unset it after display.
*
* @param string $message The message to display.
* @param string $type The type of message (e.g., 'error', 'notice').
* @param bool $unset Whether to unset the message after display.
*/
function renderMessage(&$message, $type, $unset = false) {
if (isset($message)) {
echo "\t\t<div class=\"{$type}\">" . $message . "</div>\n";
if ($unset) {
$message = null;
}
}
}
?>

View File

@ -0,0 +1,8 @@
<?php
if (isset($messages) && is_array($messages)) {
foreach ($messages as $msg) {
echo Messages::render($msg['category'], $msg['key'], $msg['custom_message'] ?? null, $msg['dismissible'] ?? false, $msg['small'] ?? false);
}
}
?>

View File

@ -0,0 +1,36 @@
<?php
// Message strings for translation
return [
'LOGIN' => [
'LOGIN_SUCCESS' => 'Login successful.',
'LOGIN_FAILED' => 'Login failed. Please check your credentials.',
'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.',
'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.',
'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.',
'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.',
],
'SECURITY' => [
'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.',
'WHITELIST_ADD_ERROR' => 'Failed to add IP to whitelist. Please check the IP format.',
'WHITELIST_REMOVE_SUCCESS' => 'IP address successfully removed from whitelist.',
'WHITELIST_REMOVE_ERROR' => 'Failed to remove IP from whitelist.',
'BLACKLIST_ADD_SUCCESS' => 'IP address successfully added to blacklist.',
'BLACKLIST_ADD_ERROR' => 'Failed to add IP to blacklist. Please check the IP format.',
'BLACKLIST_REMOVE_SUCCESS' => 'IP address successfully removed from blacklist.',
'BLACKLIST_REMOVE_ERROR' => 'Failed to remove IP from blacklist.',
'RATE_LIMIT_INFO' => 'Rate limiting is active. This helps protect against brute force attacks.',
'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.',
'IP_REQUIRED' => 'IP address is required.',
],
'REGISTER' => [
'SUCCESS' => 'Registration successful. You can log in now.',
'FAILED' => 'Registration failed: %s',
'DISABLED' => 'Registration is disabled.',
],
'SYSTEM' => [
'DB_ERROR' => 'Error connecting to the database: %s',
'DB_CONNECT_ERROR' => 'Error connecting to DB: %s',
'DB_UNKNOWN_TYPE' => 'Error: unknown database type "%s"',
],
];

View File

@ -0,0 +1,17 @@
<?php
// Get any flash messages from previous request
$flash_messages = Messages::getFlash();
if (!empty($flash_messages)) {
$messages = array_merge($messages, array_map(function($flash) {
return [
'category' => $flash['category'],
'key' => $flash['key'],
'custom_message' => $flash['custom_message'] ?? null,
'dismissible' => $flash['dismissible'] ?? false,
'small' => $flash['small'] ?? false
];
}, $flash_messages));
}
?>

View File

@ -21,11 +21,24 @@ if (isset($_REQUEST['until_time'])) {
$until_time = htmlspecialchars($_REQUEST['until_time']);
}
if (isset($_SESSION['notice'])) {
$notice = htmlspecialchars($_SESSION['notice']); // 'notice' for all non-critical messages
// sanitize session vars
if (isset($_SESSION)) {
foreach ($_SESSION as $key => $value) {
if (is_string($value)) {
$_SESSION[$key] = htmlspecialchars($value);
}
}
}
if (isset($_SESSION['error'])) {
$error = htmlspecialchars($_SESSION['error']); // 'error' for errors
// hosts
if (isset($_POST['address'])) {
$address = htmlspecialchars($_POST['address']);
}
if (isset($_POST['port'])) {
$port = htmlspecialchars($_POST['port']);
}
if (isset($_POST['name'])) {
$name = htmlspecialchars($_POST['name']);
}
// agents
@ -38,6 +51,9 @@ if (isset($_POST['url'])) {
if (isset($_POST['secret_key'])) {
$secret_key = htmlspecialchars($_POST['secret_key']);
}
if (isset($_POST['check_period'])) {
$check_period = htmlspecialchars($_POST['check_period']);
}
// platforms
if (isset($_POST['name'])) {

View File

@ -1,5 +1,17 @@
<?php
/**
* Agent cache management
*
* This page ("agents") handles caching for agents. It allows storing, clearing, and retrieving
* agent-related data in the session using AJAX requests. The cache is stored with a timestamp
* to allow time-based invalidation if needed.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
$action = $_REQUEST['action'] ?? '';
$agent = $_REQUEST['agent'] ?? '';
require '../app/classes/agent.php';

View File

@ -1,85 +1,107 @@
<?php
/**
* Components information
*
* This page ("components") retrieves and displays information about Jitsi components events.
* Allows filtering by component ID, name, or event name, and listing within a specified time range.
* 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
$db = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
// specify time range
include '../app/helpers/time_range.php';
// if DB connection has error, display it and stop here
if ($response['db'] === null) {
Messages::flash('ERROR', 'DEFAULT', $response['error']);
// 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
$jitsi_component = !empty($_REQUEST['name']) ? "'" . $_REQUEST['name'] . "'" : 'jitsi_component';
$component_id = !empty($_REQUEST['id']) ? "'" . $_REQUEST['id'] . "'" : 'component_id';
$event_type = !empty($_REQUEST['event']) ? "'" . $_REQUEST['event'] . "'" : 'event_type';
//
// Component events listings
//
// list of all component events (default)
$componentObject = new Component($db);
// pagination variables
$items_per_page = 15;
$browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
// prepare the result
$search = $componentObject->jitsiComponents($jitsi_component, $component_id, $event_type, $from_time, $until_time, $offset, $items_per_page);
$search_all = $componentObject->jitsiComponents($jitsi_component, $component_id, $event_type, $from_time, $until_time);
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$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['filter'] = true;
$widget['pagination'] = 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>';
// otherwise if DB connection is OK, go on
} 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'];
}
$db = $response['db'];
// display the widget
include '../app/templates/event-list-components.php';
// specify time range
include '../app/helpers/time_range.php';
// 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
$jitsi_component = !empty($_REQUEST['name']) ? "'" . $_REQUEST['name'] . "'" : 'jitsi_component';
$component_id = !empty($_REQUEST['id']) ? "'" . $_REQUEST['id'] . "'" : 'component_id';
$event_type = !empty($_REQUEST['event']) ? "'" . $_REQUEST['event'] . "'" : 'event_type';
//
// Component events listings
//
// list of all component events (default)
$componentObject = new Component($db);
// pagination variables
$items_per_page = 15;
$browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
// prepare the result
$search = $componentObject->jitsiComponents($jitsi_component, $component_id, $event_type, $from_time, $until_time, $offset, $items_per_page);
$search_all = $componentObject->jitsiComponents($jitsi_component, $component_id, $event_type, $from_time, $until_time);
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$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['filter'] = true;
$widget['pagination'] = 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/event-list-components.php';
}
?>

View File

@ -1,149 +1,170 @@
<?php
/**
* Conference information
*
* This page ("conferences") retrieves and displays information about conferences.
* Allows filtering by conference ID or name, and listing within a specified time range.
* 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
$db = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
// specify time range
include '../app/helpers/time_range.php';
// if DB connection has error, display it and stop here
if ($response['db'] === null) {
Messages::flash('ERROR', 'DEFAULT', $response['error']);
// conference id/name are specified when searching specific conference(s)
// 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
//$conferenceName = !empty($_REQUEST['name']) ? "'" . $_REQUEST['name'] . "'" : 'conference_name';
//$conferenceId = !empty($_REQUEST['id']) ? "'" . $_REQUEST['id'] . "'" : 'conference_id';
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'];
// otherwise if DB connection is OK, go on
} else {
unset($conferenceId);
unset($conferenceName);
}
$db = $response['db'];
// specify time range
include '../app/helpers/time_range.php';
//
// Conference listings
//
// conference id/name are specified when searching specific conference(s)
// 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
//$conferenceName = !empty($_REQUEST['name']) ? "'" . $_REQUEST['name'] . "'" : 'conference_name';
//$conferenceId = !empty($_REQUEST['id']) ? "'" . $_REQUEST['id'] . "'" : 'conference_id';
$conferenceObject = new Conference($db);
// pagination variables
$items_per_page = 15;
$browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
// search and list specific conference ID
if (isset($conferenceId)) {
$search = $conferenceObject->conferenceById($conferenceId, $from_time, $until_time, $offset, $items_per_page);
$search_all = $conferenceObject->conferenceById($conferenceId, $from_time, $until_time);
// search and list specific conference name
} elseif (isset($conferenceName)) {
$search = $conferenceObject->conferenceByName($conferenceName, $from_time, $until_time, $offset, $items_per_page);
$search_all = $conferenceObject->conferenceByName($conferenceName, $from_time, $until_time);
// list of all conferences (default)
} else {
$search = $conferenceObject->conferencesAllFormatted($from_time, $until_time, $offset, $items_per_page);
$search_all = $conferenceObject->conferencesAllFormatted($from_time, $until_time);
}
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$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);
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);
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'Conferences';
$widget['collapsible'] = false;
$widget['collapsed'] = false;
$widget['filter'] = true;
$widget['pagination'] = 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'];
}
//
// Conference listings
//
// display the widget
include '../app/templates/event-list-conferences.php';
$conferenceObject = new Conference($db);
// pagination variables
$items_per_page = 15;
$browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
// search and list specific conference ID
if (isset($conferenceId)) {
$search = $conferenceObject->conferenceById($conferenceId, $from_time, $until_time, $offset, $items_per_page);
$search_all = $conferenceObject->conferenceById($conferenceId, $from_time, $until_time);
// search and list specific conference name
} elseif (isset($conferenceName)) {
$search = $conferenceObject->conferenceByName($conferenceName, $from_time, $until_time, $offset, $items_per_page);
$search_all = $conferenceObject->conferenceByName($conferenceName, $from_time, $until_time);
// list of all conferences (default)
} else {
$search = $conferenceObject->conferencesAllFormatted($from_time, $until_time, $offset, $items_per_page);
$search_all = $conferenceObject->conferencesAllFormatted($from_time, $until_time);
}
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$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['pagination'] = 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/event-list-conferences.php';
}
?>

View File

@ -1,28 +1,69 @@
<?php
/**
* Configuration management.
*
* This page ("config") handles configuration by adding, editing, and deleting platforms,
* hosts, agents, and the configuration file itself.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
$action = $_REQUEST['action'] ?? '';
$agent = $_REQUEST['agent'] ?? '';
$host = $_REQUEST['host'] ?? '';
require '../app/classes/config.php';
require '../app/classes/host.php';
require '../app/classes/agent.php';
$configObject = new Config();
$hostObject = new Host($dbWeb);
$agentObject = new Agent($dbWeb);
// if a form is submitted, it's from the edit page
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
/**
* Handles form submissions from editing page
*/
// FIXME - if editing the flat file is no more needed, remove this
// // load the config file and initialize a copy
// $content = file_get_contents($config_file);
// $updatedContent = $content;
// 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";
}
}
// new host adding
} elseif (isset($_POST['new']) && isset($_POST['item']) && $_POST['new'] === 'true' && $_POST['item'] === 'host') {
$newHost = [
'address' => $address,
'port' => $port,
'platform_id' => $platform_id,
'name' => $name,
];
$result = $hostObject->addHost($newHost);
if ($result === true) {
$_SESSION['notice'] = "New Jilo host added.";
} else {
$_SESSION['error'] = "Adding the host failed. Error: $result";
}
// new agent adding
if (isset($_POST['new']) && isset($_POST['item']) && $_POST['new'] === 'true' && $_POST['item'] === 'agent') {
} 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) {
@ -38,7 +79,21 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
'jitsi_url' => $_POST['jitsi_url'],
'jilo_database' => $_POST['jilo_database'],
];
$platformObject->addPlatform($newPlatform);
$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') {
@ -52,7 +107,27 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// deleting a platform
} elseif (isset($_POST['delete']) && $_POST['delete'] === 'true') {
$platform = $_POST['platform'];
$platformObject->deletePlatform($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'])) {
@ -61,6 +136,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
'agent_type_id' => $type,
'url' => $url,
'secret_key' => $secret_key,
'check_period' => $check_period,
];
$result = $agentObject->editAgent($platform_id, $updatedAgent);
if ($result === true) {
@ -77,85 +153,104 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
'jitsi_url' => $_POST['jitsi_url'],
'jilo_database' => $_POST['jilo_database'],
];
$platformObject->editPlatform($platform, $updatedPlatform);
$result = $platformObject->editPlatform($platform, $updatedPlatform);
if ($result === true) {
$_SESSION['notice'] = "Platform \"{$_REQUEST['name']}\" edited.";
} else {
$_SESSION['error'] = "Editing the platform failed. Error: $result";
}
}
// FIXME - if this is not needed for editing the flat file, remove it
// // 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");
header("Location: $app_root?page=config&item=$item");
exit();
// no form submitted, show the templates
} else {
/**
* Handles GET requests to display templates.
*/
// $item - config.js and interface_config.js are special case; remote loaded files
switch ($item) {
case 'configjs':
$mode = $_REQUEST['mode'] ?? '';
$raw = ($mode === 'raw');
$platformConfigjs = $configObject->getPlatformConfigjs($platformDetails[0]['jitsi_url'], $raw);
include '../app/templates/config-list-configjs.php';
break;
case 'interfaceconfigjs':
$mode = $_REQUEST['mode'] ?? '';
$raw = ($mode === 'raw');
$platformInterfaceConfigjs = $configObject->getPlatformInterfaceConfigjs($platformDetails[0]['jitsi_url'], $raw);
include '../app/templates/config-list-interfaceconfigjs.php';
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;
// if there is no $item, we work on the local config DB
default:
switch ($action) {
case 'add-agent':
$jilo_agent_types = $agentObject->getAgentTypes();
include '../app/templates/config-add-agent.php';
break;
case 'add':
include '../app/templates/config-add-platform.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';
} else {
include '../app/templates/config-edit-platform.php';
}
break;
case 'delete':
if (isset($_GET['agent'])) {
$agentDetails = $agentObject->getAgentDetails($platform_id, $agent);
include '../app/templates/config-delete-agent.php';
} else {
include '../app/templates/config-delete-platform.php';
}
break;
default:
if ($userObject->hasRight($user_id, 'view config file')) {
include '../app/templates/config-list.php';
} else {
include '../app/templates/error-unauthorized.php';
}
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();
}
}

View File

@ -1,204 +1,232 @@
<?php
/**
* Main dashboard file for displaying conference statistics.
*
* This page ("dashboard") connects to the database and displays various widgets:
* 1. Monthly statistics for the past year.
* 2. Conferences from the last 2 days.
* 3. The most recent 10 conferences.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
require '../app/classes/conference.php';
require '../app/classes/participant.php';
// connect to database
$db = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
// if DB connection has error, display it and stop here
if ($response['db'] === null) {
Messages::flash('ERROR', 'DEFAULT', $response['error']);
// otherwise if DB connection is OK, go on
} else {
$db = $response['db'];
$conferenceObject = new Conference($db);
$participantObject = new Participant($db);
//
// dashboard widget listings
//
////
// monthly usage
$conferenceObject = new Conference($db);
$participantObject = new Participant($db);
// monthly conferences for the last year
$fromMonth = (new DateTime())->sub(new DateInterval('P1Y'));
$fromMonth->modify('first day of this month');
$thisMonth = new DateTime();
$from_time = $fromMonth->format('Y-m-d');
$until_time = $thisMonth->format('Y-m-d');
$widget['records'] = array();
// loop 1 year in the past
$i = 0;
while ($fromMonth < $thisMonth) {
$untilMonth = clone $fromMonth;
$untilMonth->modify('last day of this month');
/**
* Monthly usage statistics for the last year.
*
* Retrieves conference and participant numbers for each month within the past year.
*/
// monthly conferences for the last year
$fromMonth = (new DateTime())->sub(new DateInterval('P1Y'));
$fromMonth->modify('first day of this month');
$thisMonth = new DateTime();
$from_time = $fromMonth->format('Y-m-d');
$until_time = $untilMonth->format('Y-m-d');
$until_time = $thisMonth->format('Y-m-d');
$searchConferenceNumber = $conferenceObject->conferenceNumber($from_time, $until_time);
$searchParticipantNumber = $participantObject->participantNumber($from_time, $until_time);
// pretty format for displaying the month in the widget
$month = $fromMonth->format('F Y');
// populate the records
$widget['records'][$i] = array(
'from_time' => $from_time,
'until_time' => $until_time,
'table_headers' => $month,
'conferences' => $searchConferenceNumber[0]['conferences'],
'participants' => $searchParticipantNumber[0]['participants'],
);
// move everything one month in future
$untilMonth->add(new DateInterval('P1M'));
$fromMonth->add(new DateInterval('P1M'));
$i++;
}
$time_range_specified = true;
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'LastYearMonths';
$widget['title'] = 'Conferences monthly stats for the last year';
$widget['collapsible'] = true;
$widget['collapsed'] = false;
$widget['filter'] = false;
if (!empty($searchConferenceNumber) && !empty($searchParticipantNumber)) {
$widget['full'] = true;
}
$widget['pagination'] = false;
// display the widget
include '../app/templates/widget-monthly.php';
////
// conferences in last 2 days
// time range limit
$from_time = date('Y-m-d', time() - 60 * 60 * 24 * 2);
$until_time = date('Y-m-d', time());
$time_range_specified = true;
// prepare the result
$search = $conferenceObject->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 = '';
}
$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'] = 'LastDays';
$widget['title'] = 'Conferences for the last 2 days';
$widget['collapsible'] = true;
$widget['collapsed'] = false;
$widget['filter'] = false;
if (!empty($conferences['records'])) {
$widget['full'] = true;
$widget['table_headers'] = array_keys($conferences['records'][0]);
$widget['table_records'] = $conferences['records'];
}
$widget['pagination'] = false;
// display the widget
include '../app/templates/widget.php';
////
// last 10 conferences
// all time
$from_time = '0000-01-01';
$until_time = '9999-12-31';
$time_range_specified = false;
// number of conferences to show
$conference_number = 10;
// prepare the result
$search = $conferenceObject->conferencesAllFormatted($from_time, $until_time);
if (!empty($search)) {
$conferences = array();
$conferences['records'] = array();
$widget['records'] = array();
// loop 1 year in the past
$i = 0;
foreach ($search as $item) {
extract($item);
while ($fromMonth < $thisMonth) {
// 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 = '';
}
$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
$untilMonth = clone $fromMonth;
$untilMonth->modify('last day of this month');
$from_time = $fromMonth->format('Y-m-d');
$until_time = $untilMonth->format('Y-m-d');
$searchConferenceNumber = $conferenceObject->conferenceNumber($from_time, $until_time);
$searchParticipantNumber = $participantObject->participantNumber($from_time, $until_time);
// pretty format for displaying the month in the widget
$month = $fromMonth->format('F Y');
// populate the records
$widget['records'][$i] = array(
'from_time' => $from_time,
'until_time' => $until_time,
'table_headers' => $month,
'conferences' => $searchConferenceNumber[0]['conferences'],
'participants' => $searchParticipantNumber[0]['participants'],
);
// populate the result array
array_push($conferences['records'], $conference_record);
// we only take the first 10 results
// move everything one month in future
$untilMonth->add(new DateInterval('P1M'));
$fromMonth->add(new DateInterval('P1M'));
$i++;
if ($i == 10) break;
}
$time_range_specified = true;
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'LastYearMonths';
$widget['title'] = 'Conferences monthly stats for the last year';
$widget['collapsible'] = true;
$widget['collapsed'] = false;
$widget['filter'] = false;
if (!empty($searchConferenceNumber) && !empty($searchParticipantNumber)) {
$widget['full'] = true;
}
$widget['pagination'] = false;
// display the widget
include '../app/templates/widget-monthly.php';
/**
* Conferences in the last 2 days.
*
* Displays a summary of all conferences held in the past 48 hours.
*/
// time range limit
$from_time = date('Y-m-d', time() - 60 * 60 * 24 * 2);
$until_time = date('Y-m-d', time());
$time_range_specified = true;
// prepare the result
$search = $conferenceObject->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 = '';
}
$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'] = 'LastDays';
$widget['title'] = 'Conferences for the last 2 days';
$widget['collapsible'] = true;
$widget['collapsed'] = false;
$widget['filter'] = false;
if (!empty($conferences['records'])) {
$widget['full'] = true;
$widget['table_headers'] = array_keys($conferences['records'][0]);
$widget['table_records'] = $conferences['records'];
}
$widget['pagination'] = false;
// display the widget
include '../app/templates/widget.php';
/**
* Last 10 conferences.
*
* Displays the 10 most recent conferences in the database.
*/
// all time
$from_time = '0000-01-01';
$until_time = '9999-12-31';
$time_range_specified = false;
// number of conferences to show
$conference_number = 10;
// prepare the result
$search = $conferenceObject->conferencesAllFormatted($from_time, $until_time);
if (!empty($search)) {
$conferences = array();
$conferences['records'] = array();
$i = 0;
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 = '';
}
$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);
// we only take the first 10 results
$i++;
if ($i == 10) break;
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'LastConferences';
$widget['title'] = 'The last ' . $conference_number . ' conferences';
$widget['collapsible'] = true;
$widget['collapsed'] = false;
$widget['filter'] = false;
$widget['pagination'] = false;
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';
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'LastConferences';
$widget['title'] = 'The last ' . $conference_number . ' conferences';
$widget['collapsible'] = true;
$widget['collapsed'] = false;
$widget['filter'] = false;
$widget['pagination'] = false;
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';
?>

207
app/pages/data.php 100644
View File

@ -0,0 +1,207 @@
<?php
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
$action = $_REQUEST['action'] ?? '';
$agent = $_REQUEST['agent'] ?? '';
require '../app/classes/config.php';
require '../app/classes/agent.php';
require '../app/classes/conference.php';
$configObject = new Config();
$agentObject = new Agent($dbWeb);
// connect to Jilo database
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
// if DB connection has error, display it and stop here
if ($response['db'] === null) {
Messages::flash('ERROR', 'DEFAULT', $response['error']);
// otherwise if DB connection is OK, go on
} else {
$db = $response['db'];
$conferenceObject = new Conference($db);
switch ($item) {
case 'graphs':
// Connect to Jilo database for log data
$jilo_response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
if ($jilo_response['db'] === null) {
Messages::flash('ERROR', 'DEFAULT', $jilo_response['error']);
break;
}
$jilo_db = $jilo_response['db'];
// Get date range for the last 7 days
$from_time = date('Y-m-d', strtotime('-7 days'));
$until_time = date('Y-m-d');
// Define graphs to show
$graphs = [
[
'graph_name' => 'conferences',
'graph_title' => 'Conferences in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time',
'datasets' => []
],
[
'graph_name' => 'participants',
'graph_title' => 'Participants in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time',
'datasets' => []
]
];
// Get Jitsi API data
$conferences_api = $agentObject->getHistoricalData(
$platform_id,
'jicofo',
'conferences',
$from_time,
$until_time
);
$graphs[0]['datasets'][] = [
'data' => $conferences_api,
'label' => 'Conferences from Jitsi API',
'color' => 'rgba(75, 192, 192, 1)'
];
// Get conference data from logs
$conferences_logs = $conferenceObject->conferenceNumber(
$from_time,
$until_time
);
$graphs[0]['datasets'][] = [
'data' => $conferences_logs,
'label' => 'Conferences from Logs',
'color' => 'rgba(255, 99, 132, 1)'
];
// Get participants data
$participants_api = $agentObject->getHistoricalData(
$platform_id,
'jicofo',
'participants',
$from_time,
$until_time
);
$graphs[1]['datasets'][] = [
'data' => $participants_api,
'label' => 'Participants from Jitsi API',
'color' => 'rgba(75, 192, 192, 1)'
];
// Prepare data for template
$graph = $graphs;
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'Graphs';
$widget['title'] = 'Jitsi graphs';
include '../app/templates/graphs-combined.php';
break;
case 'latest':
// Define metrics to display
$metrics = [
'Basic stats' => [
'conferences' => ['label' => 'Current conferences', 'link' => 'conferences'],
'participants' => ['label' => 'Current participants', 'link' => 'participants'],
'total_conferences_created' => ['label' => 'Total conferences created'],
'total_participants' => ['label' => 'Total participants']
],
'Bridge stats' => [
'bridge_selector.bridge_count' => ['label' => 'Bridge count'],
'bridge_selector.operational_bridge_count' => ['label' => 'Operational bridges'],
'bridge_selector.in_shutdown_bridge_count' => ['label' => 'Bridges in shutdown']
],
'Jibri stats' => [
'jibri_detector.count' => ['label' => 'Jibri count'],
'jibri_detector.available' => ['label' => 'Jibri idle'],
'jibri.live_streaming_active' => ['label' => 'Jibri active streaming'],
'jibri.recording_active' => ['label' => 'Jibri active recording'],
],
'System stats' => [
'threads' => ['label' => 'Threads'],
'stress_level' => ['label' => 'Stress level'],
'version' => ['label' => 'Version']
]
];
// Get latest data for all the agents
$agents = ['jvb', 'jicofo', 'jibri', 'prosody', 'nginx'];
$widget['records'] = [];
// Initialize records for each agent
foreach ($agents as $agent) {
$record = [
'table_headers' => strtoupper($agent),
'metrics' => [],
'timestamp' => null
];
// Fetch all metrics for this agent
foreach ($metrics as $section => $section_metrics) {
foreach ($section_metrics as $metric => $metricConfig) {
$data = $agentObject->getLatestData($platform_id, $agent, $metric);
if ($data !== null) {
$record['metrics'][$section][$metric] = [
'value' => $data['value'],
'label' => $metricConfig['label'],
'link' => isset($metricConfig['link']) ? $metricConfig['link'] : null
];
// Use the most recent timestamp
if ($record['timestamp'] === null || strtotime($data['timestamp']) > strtotime($record['timestamp'])) {
$record['timestamp'] = $data['timestamp'];
}
}
}
}
if (!empty($record['metrics'])) {
$widget['records'][] = $record;
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'LatestData';
$widget['title'] = 'Latest data from Jilo Agents';
$widget['collapsible'] = false;
$widget['collapsed'] = false;
$widget['filter'] = false;
$widget['metrics'] = $metrics; // Pass metrics configuration to template
if (!empty($widget['records'])) {
$widget['full'] = true;
}
$widget['pagination'] = false;
include '../app/templates/latest-data.php';
break;
case 'configjs':
$mode = $_REQUEST['mode'] ?? '';
$raw = ($mode === 'raw');
$platformConfigjs = $configObject->getPlatformConfigjs($platformDetails[0]['jitsi_url'], $raw);
include '../app/templates/data-configjs.php';
break;
case 'interfaceconfigjs':
$mode = $_REQUEST['mode'] ?? '';
$raw = ($mode === 'raw');
$platformInterfaceConfigjs = $configObject->getPlatformInterfaceConfigjs($platformDetails[0]['jitsi_url'], $raw);
include '../app/templates/data-interfaceconfigjs.php';
break;
default:
}
}
?>

View File

@ -1,49 +0,0 @@
<?php
// FIXME example data
$one = date('Y-m-d',strtotime("-5 days"));
$two = date('Y-m-d',strtotime("-4 days"));
$three = date('Y-m-d',strtotime("-2 days"));
$four = date('Y-m-d',strtotime("-1 days"));
$graph[0]['data0'] = [
['date' => $one, 'value' => 10],
['date' => $two, 'value' => 20],
['date' => $three, 'value' => 15],
['date' => $four, 'value' => 25],
];
$graph[0]['data1'] = [
['date' => $one, 'value' => 12],
['date' => $two, 'value' => 23],
['date' => $three, 'value' => 11],
['date' => $four, 'value' => 27],
];
$graph[0]['graph_name'] = 'conferences';
$graph[0]['graph_title'] = 'Conferences in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time';
$graph[0]['graph_data0_label'] = 'Conferences from Jitsi logs (Jilo)';
$graph[0]['graph_data1_label'] = 'Conferences from Jitsi API (Jilo Agents)';
$graph[1]['data0'] = [
['date' => $one, 'value' => 20],
['date' => $two, 'value' => 30],
['date' => $three, 'value' => 15],
['date' => $four, 'value' => 55],
];
$graph[1]['data1'] = [
['date' => $one, 'value' => 22],
['date' => $two, 'value' => 33],
['date' => $three, 'value' => 11],
['date' => $four, 'value' => 57],
];
$graph[1]['graph_name'] = 'participants';
$graph[1]['graph_title'] = 'Participants in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time';
$graph[1]['graph_data0_label'] = 'Participants from Jitsi logs (Jilo)';
$graph[1]['graph_data1_label'] = 'Participants from Jitsi API (Jilo Agents)';
include '../app/templates/graphs-combined.php';
?>

View File

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

View File

@ -1,29 +0,0 @@
<?php
require '../app/classes/agent.php';
$agentObject = new Agent($dbWeb);
$latestJvbConferences = $agentObject->getLatestData($platform_id, 'jvb', 'conferences');
$latestJvbParticipants = $agentObject->getLatestData($platform_id, 'jvb', 'participants');
$latestJicofoConferences = $agentObject->getLatestData($platform_id, 'jicofo', 'conferences');
$latestJicofoParticipants = $agentObject->getLatestData($platform_id, 'jicofo', 'participants');
$widget['records'] = array();
// 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;
if (!empty($latestJvbConferences) && !empty($latestJvbParticipants) && !empty($latestJicofoConferences) && !empty($latestJicofoParticipants)) {
$widget['full'] = true;
}
$widget['pagination'] = true;
include '../app/templates/latest-data.php';
?>

View File

@ -1,5 +1,18 @@
<?php
/**
* User login
*
* This page ("login") handles user login, session management, cookie handling, and error logging.
* Supports "remember me" functionality to extend session duration.
*
* Actions Performed:
* - Validates login credentials.
* - Manages session and cookies based on "remember me" option.
* - Logs successful and failed login attempts.
* - Displays login form and optional custom messages.
*/
// clear the global error var before login
unset($error);
@ -8,61 +21,88 @@ try {
// connect to database
$dbWeb = connectDB($config);
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
$username = $_POST['username'];
$password = $_POST['password'];
// Initialize RateLimiter
require_once '../app/classes/ratelimiter.php';
$rateLimiter = new RateLimiter($dbWeb['db']);
// login successful
if ( $userObject->login($username, $password) ) {
// if remember_me is checked, max out the session
if (isset($_POST['remember_me'])) {
// 30*24*60*60 = 30 days
$cookie_lifetime = 30 * 24 * 60 * 60;
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
$gc_maxlifetime = 30 * 24 * 60 * 60;
} else {
// 0 - session end on browser close
// 1440 - 24 minutes (default)
$cookie_lifetime = 0;
$setcookie_lifetime = 0;
$gc_maxlifetime = 1440;
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
try {
$username = $_POST['username'];
$password = $_POST['password'];
// Check if IP is blacklisted
if ($rateLimiter->isIpBlacklisted($user_IP)) {
throw new Exception(Messages::get('LOGIN', 'IP_BLACKLISTED')['message']);
}
// set session lifetime and cookies
setcookie('username', $username, [
'expires' => $setcookie_lifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Check rate limiting (but skip if IP is whitelisted)
if (!$rateLimiter->isIpWhitelisted($user_IP)) {
$attempts = $rateLimiter->getRecentAttempts($user_IP);
if ($attempts >= $rateLimiter->maxAttempts) {
throw new Exception(Messages::get('LOGIN', 'LOGIN_BLOCKED')['message']);
}
}
// redirect to index
$_SESSION['notice'] = "Login successful";
$user_id = $userObject->getUserId($username)[0]['id'];
$logObject->insertLog($user_id, "Login: User \"$username\" logged in. IP: $user_IP", 'user');
header('Location: index.php');
exit();
// login successful
if ( $userObject->login($username, $password) ) {
// if remember_me is checked, max out the session
if (isset($_POST['remember_me'])) {
// 30*24*60*60 = 30 days
$cookie_lifetime = 30 * 24 * 60 * 60;
$setcookie_lifetime = time() + 30 * 24 * 60 * 60;
$gc_maxlifetime = 30 * 24 * 60 * 60;
} else {
// 0 - session end on browser close
// 1440 - 24 minutes (default)
$cookie_lifetime = 0;
$setcookie_lifetime = 0;
$gc_maxlifetime = 1440;
}
// login failed
} else {
$_SESSION['error'] = "Login failed.";
$user_id = $userObject->getUserId($username)[0]['id'];
$logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP", 'user');
header('Location: index.php');
exit();
// set session lifetime and cookies
setcookie('username', $username, [
'expires' => $setcookie_lifetime,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict'
]);
// Log successful login
$user_id = $userObject->getUserId($username)[0]['id'];
$logObject->insertLog($user_id, "Login: User \"$username\" logged in. IP: $user_IP", 'user');
// Set success message and redirect
Messages::flash('LOGIN', 'LOGIN_SUCCESS', null, true);
header('Location: ' . htmlspecialchars($app_root));
exit();
} else {
throw new Exception(Messages::get('LOGIN', 'LOGIN_FAILED')['message']);
}
} catch (Exception $e) {
// Log the failed attempt
Messages::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');
}
}
}
} catch (Exception $e) {
$error = getError('There was an unexpected error. Please try again.', $e->getMessage());
Messages::flash('ERROR', 'DEFAULT', 'There was an unexpected error. Please try again.');
}
// Show configured login message if any
if (!empty($config['login_message'])) {
$notice = $config['login_message'];
include '../app/templates/block-message.php';
echo Messages::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false);
}
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Load the template
include '../app/templates/form-login.php';
?>

View File

@ -1,8 +1,23 @@
<?php
//
// logs listings
//
/**
* Logs listings
*
* This page ("logs") retrieves and displays logs for a specified user within a time range.
* It supports pagination and filtering, and generates a widget to display the logs.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Check for rights; user or system
if (($userObject->hasRight($user_id, 'superuser') ||
$userObject->hasRight($user_id, 'view app logs'))) {
$scope = 'system';
} else {
$scope = 'user';
}
// specify time range
include '../app/helpers/time_range.php';
@ -13,9 +28,6 @@ $browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
// user or system
$scope = 'user';
// prepare the result
$search = $logObject->readLog($user_id, $scope, $offset, $items_per_page);
$search_all = $logObject->readLog($user_id, $scope);

View File

@ -1,155 +1,177 @@
<?php
/**
* Participants information
*
* This page ("participants") retrieves and displays participant information for conferences.
* Allows filtering by participant ID, name, or IP address, and listing within a specified time range.
* 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
$db = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
$response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id);
// specify time range
include '../app/helpers/time_range.php';
// if DB connection has error, display it and stop here
if ($response['db'] === null) {
Messages::flash('ERROR', 'DEFAULT', $response['error']);
// participant id/name/IP are specified when searching specific participant(s)
// participant name - this is 'stats_id' in the db
// either id, name, OR IP - in that order
// we use $_REQUEST, so that both links and forms work
if (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
$participantId = $_REQUEST['id'];
unset($_REQUEST['name']);
unset($participantName);
} elseif (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
unset($participantId);
$participantName = $_REQUEST['name'];
} elseif (isset($_REQUEST['ip']) && $_REQUEST['ip'] != '') {
unset($participantId);
$participantIp = $_REQUEST['ip'];
// otherwise if DB connection is OK, go on
} else {
unset($participantId);
unset($participantName);
}
$db = $response['db'];
// specify time range
include '../app/helpers/time_range.php';
//
// Participant listings
//
$participantObject = new Participant($db);
// pagination variables
$items_per_page = 15;
$browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
// search and list specific participant ID
if (isset($participantId)) {
$search = $participantObject->conferenceByParticipantId($participantId, $from_time, $until_time, $offset, $items_per_page);
$search_all = $participantObject->conferenceByParticipantId($participantId, $from_time, $until_time);
// search and list specific participant name (stats_id)
} elseif (isset($participantName)) {
$search = $participantObject->conferenceByParticipantName($participantName, $from_time, $until_time, $offset, $items_per_page);
$search_all = $participantObject->conferenceByParticipantName($participantName, $from_time, $until_time);
// search and list specific participant IP
} elseif (isset($participantIp)) {
$search = $participantObject->conferenceByParticipantIP($participantIp, $from_time, $until_time, $offset, $items_per_page);
$search_all = $participantObject->conferenceByParticipantIP($participantIp, $from_time, $until_time);
// list of all participants (default)
} else {
// prepare the result
$search = $participantObject->participantsAll($from_time, $until_time, $offset, $items_per_page);
$search_all = $participantObject->participantsAll($from_time, $until_time);
}
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$participants = array();
$participants['records'] = array();
foreach ($search as $item) {
extract($item);
// search and list specific participant ID
if (isset($participantId)) {
$participant_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 participant name (stats_id)
} elseif (isset($participantName)) {
$participant_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 participant IP
} elseif (isset($participantIp)) {
$participant_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 participants (default)
} else {
$participant_record = array(
// assign title to the field in the array record
'component' => $jitsi_component,
'participant ID' => $endpoint_id,
'conference ID' => $conference_id
);
}
// populate the result array
array_push($participants['records'], $participant_record);
// participant id/name/IP are specified when searching specific participant(s)
// participant name - this is 'stats_id' in the db
// either id, name, OR IP - in that order
// we use $_REQUEST, so that both links and forms work
if (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
$participantId = $_REQUEST['id'];
unset($_REQUEST['name']);
unset($participantName);
} elseif (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
unset($participantId);
$participantName = $_REQUEST['name'];
} elseif (isset($_REQUEST['ip']) && $_REQUEST['ip'] != '') {
unset($participantId);
$participantIp = $_REQUEST['ip'];
} else {
unset($participantId);
unset($participantName);
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'Participants';
$widget['collapsible'] = false;
$widget['collapsed'] = false;
$widget['filter'] = true;
$widget['pagination'] = true;
// widget title
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
$widget['title'] = 'Conferences with participant name (stats_id) matching "<strong>' . $_REQUEST['name'] . '"</strong>';
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
$widget['title'] = 'Conference with participant ID matching "<strong>' . $_REQUEST['id'] . '"</strong>';
} elseif (isset($participantIp)) {
$widget['title'] = 'Conference with participant IP matching "<strong>' . $participantIp . '"</strong>';
} else {
$widget['title'] = 'All participants';
}
// widget records
if (!empty($participants['records'])) {
$widget['full'] = true;
$widget['table_headers'] = array_keys($participants['records'][0]);
$widget['table_records'] = $participants['records'];
}
//
// Participant listings
//
// display the widget
include '../app/templates/widget.php';
$participantObject = new Participant($db);
// pagination variables
$items_per_page = 15;
$browse_page = $_REQUEST['p'] ?? 1;
$browse_page = (int)$browse_page;
$offset = ($browse_page -1) * $items_per_page;
// search and list specific participant ID
if (isset($participantId)) {
$search = $participantObject->conferenceByParticipantId($participantId, $from_time, $until_time, $offset, $items_per_page);
$search_all = $participantObject->conferenceByParticipantId($participantId, $from_time, $until_time);
// search and list specific participant name (stats_id)
} elseif (isset($participantName)) {
$search = $participantObject->conferenceByParticipantName($participantName, $from_time, $until_time, $offset, $items_per_page);
$search_all = $participantObject->conferenceByParticipantName($participantName, $from_time, $until_time);
// search and list specific participant IP
} elseif (isset($participantIp)) {
$search = $participantObject->conferenceByParticipantIP($participantIp, $from_time, $until_time, $offset, $items_per_page);
$search_all = $participantObject->conferenceByParticipantIP($participantIp, $from_time, $until_time);
// list of all participants (default)
} else {
// prepare the result
$search = $participantObject->participantsAll($from_time, $until_time, $offset, $items_per_page);
$search_all = $participantObject->participantsAll($from_time, $until_time);
}
if (!empty($search)) {
// we get total items and number of pages
$item_count = count($search_all);
$page_count = ceil($item_count / $items_per_page);
$participants = array();
$participants['records'] = array();
foreach ($search as $item) {
extract($item);
// search and list specific participant ID
if (isset($participantId)) {
$participant_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 participant name (stats_id)
} elseif (isset($participantName)) {
$participant_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 participant IP
} elseif (isset($participantIp)) {
$participant_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 participants (default)
} else {
$participant_record = array(
// assign title to the field in the array record
'component' => $jitsi_component,
'participant ID' => $endpoint_id,
'conference ID' => $conference_id
);
}
// populate the result array
array_push($participants['records'], $participant_record);
}
}
// prepare the widget
$widget['full'] = false;
$widget['name'] = 'Participants';
$widget['collapsible'] = false;
$widget['collapsed'] = false;
$widget['filter'] = true;
$widget['pagination'] = true;
// widget title
if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') {
$widget['title'] = 'Conferences with participant name (stats_id) matching "<strong>' . $_REQUEST['name'] . '"</strong>';
} elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') {
$widget['title'] = 'Conference with participant ID matching "<strong>' . $_REQUEST['id'] . '"</strong>';
} elseif (isset($participantIp)) {
$widget['title'] = 'Conference with participant IP matching "<strong>' . $participantIp . '"</strong>';
} else {
$widget['title'] = 'All participants';
}
// widget records
if (!empty($participants['records'])) {
$widget['full'] = true;
$widget['table_headers'] = array_keys($participants['records'][0]);
$widget['table_records'] = $participants['records'];
}
// display the widget
include '../app/templates/widget.php';
}
?>

View File

@ -1,5 +1,21 @@
<?php
/**
* User profile management
*
* This page ("profile") handles user profile actions such as updating user details,
* avatar management, and assigning or removing user rights.
* It supports both form submissions and displaying profile templates.
*
* Actions handled:
* - `remove`: Remove a user's avatar.
* - `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

View File

@ -1,18 +1,21 @@
<?php
/**
* User registration
*
* This page ("register") handles user registration if the feature is enabled in the configuration.
* It accepts a POST request with a username and password, attempts to register the user,
* and redirects to the login page on success or displays an error message on failure.
*/
// registration is allowed, go on
if ($config['registration_enabled'] === true) {
// require '../app/classes/user.php';
unset($error);
try {
// connect to database
$dbWeb = connectDB($config);
// $userObject = new User($dbWeb);
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
$username = $_POST['username'];
$password = $_POST['password'];
@ -22,27 +25,30 @@ if ($config['registration_enabled'] === true) {
// redirect to login
if ($result === true) {
$_SESSION['notice'] = "Registration successful.<br />You can log in now.";
header('Location: index.php');
Messages::flash('NOTICE', 'DEFAULT', "Registration successful.<br />You can log in now.");
header('Location: ' . htmlspecialchars($app_root));
exit();
// registration fail, redirect to login
} else {
$_SESSION['error'] = "Registration failed. $result";
header('Location: index.php');
Messages::flash('ERROR', 'DEFAULT', "Registration failed. $result");
header('Location: ' . htmlspecialchars($app_root));
exit();
}
}
} catch (Exception $e) {
$error = $e->getMessage();
Messages::flash('ERROR', 'DEFAULT', $e->getMessage());
}
include '../app/templates/block-message.php';
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
// Load the template
include '../app/templates/form-register.php';
// registration disabled
} else {
$notice = 'Registration is disabled';
include '../app/templates/block-message.php';
echo Messages::render('NOTICE', 'DEFAULT', 'Registration is disabled', false);
}
?>

View File

@ -0,0 +1,113 @@
<?php
// Check if user has any of the required rights
if (!($userObject->hasRight($user_id, 'superuser') ||
$userObject->hasRight($user_id, 'edit whitelist') ||
$userObject->hasRight($user_id, 'edit blacklist') ||
$userObject->hasRight($user_id, 'edit ratelimiting'))) {
include '../app/templates/error-unauthorized.php';
exit;
}
if (!isset($currentUser)) {
include '../app/templates/error-unauthorized.php';
exit;
}
// Get current section
$section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section']) ? $_GET['section'] : 'whitelist');
// Initialize RateLimiter
require_once '../app/classes/ratelimiter.php';
$rateLimiter = new RateLimiter($dbWeb);
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
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']);
}
if (empty($_POST['ip_address'])) {
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
}
$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']);
}
if (empty($_POST['ip_address'])) {
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
}
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']);
}
if (empty($_POST['ip_address'])) {
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
}
$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']);
}
if (empty($_POST['ip_address'])) {
throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']);
}
if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $user_id)) {
throw new Exception(Messages::get('SECURITY', 'BLACKLIST_REMOVE_ERROR')['message']);
}
Messages::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS');
break;
}
} catch (Exception $e) {
$messages[] = ['category' => 'SECURITY', 'key' => 'CUSTOM_ERROR', 'custom_message' => $e->getMessage()];
Messages::flash('SECURITY', 'CUSTOM_ERROR', 'custom_message');
}
if (empty($messages)) {
// Only redirect if there were no errors
header("Location: {$app_root}?page=security&section={$section}");
exit;
}
}
// Always show rate limit info message for rate limiting section
if ($section === 'ratelimit') {
$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';
// Load the template
include '../app/templates/security.php';
?>

View File

@ -0,0 +1,72 @@
<?php
/**
* Jilo components status checks
*
* This page ("status") checks the status of various Jilo platform components
* by fetching data from agents and determining their availability.
* It generates output for each platform and agent.
*/
// Get any new messages
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
require '../app/classes/agent.php';
$agentObject = new Agent($dbWeb);
include '../app/templates/status-server.php';
// loop through all platforms to check their agents
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>';
} else {
$jilo_database_status = '<span class="text-success">OK</span>';
}
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']: '';
// 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>';
break;
}
if ($value === 'running') {
$agent_availability = '<span class="text-success">running</span>';
break;
}
}
}
} else {
$agent_availability = 'json error';
}
include '../app/templates/status-agent.php';
}
}
?>

View File

@ -22,11 +22,13 @@
// 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>

View File

@ -1,7 +0,0 @@
<?php if (isset($error)) { ?>
<div class="error"><?= $error ?></div>
<?php } ?>
<?php if (isset($notice)) { ?>
<div class="notice"><?= $notice ?></div>
<?php } ?>

View File

@ -15,12 +15,15 @@
<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']) ?>">
<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, all)</small></p>
<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>
@ -46,6 +49,17 @@
</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" />

View File

@ -0,0 +1,27 @@
<!-- widget "config file" -->
<div class="card text-center w-75 mx-lef">
<p class="h4 card-header">Jilo configuration file :: edit</p>
<div class="card-body">
<div class="card-text">
<p class="text-danger"><strong>this may break everything, use with extreme caution</strong></p>
</div>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file">
<?php
include '../app/helpers/render.php';
editConfig($config, '0');
echo "\n";
?>
<p class="text-danger"><strong>this may break everything, use with extreme caution</strong></p>
<br />
<input type="hidden" name="item" value="config_file" />
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file" />Cancel</a>
&nbsp;&nbsp;
<input type="submit" class="btn btn-danger btn-sm" value="Save" />
</form>
</div>
</div>
<!-- /widget "config file" -->

View File

@ -0,0 +1,17 @@
<!-- widget "config file" -->
<div class="card text-center w-75 mx-lef">
<p class="h4 card-header">Jilo configuration file</p>
<div class="card-body">
<?php
include '../app/helpers/render.php';
renderConfig($config, '0');
echo "\n";
?>
<br />
<a class="btn btn-outline-danger btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file&action=edit" />Edit</a>
</div>
</div>
<!-- /widget "config file" -->

View File

@ -46,6 +46,17 @@
</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']) ?>" />

View File

@ -0,0 +1,50 @@
<!-- widget "hosts" -->
<div class="card text-center w-50 mx-lef">
<p class="h4 card-header">Add new host in Jitsi platform <strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong></p>
<div class="card-body">
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=host">
<div class="row mb-3">
<div class="col-md-4 text-end">
<label for="address" class="form-label">address</label>
<span class="text-danger" style="margin-right: -12px;">*</span>
</div>
<div class="col-md-8">
<input class="form-control" type="text" name="address" value="" required autofocus />
<p class="text-start"><small>DNS name or IP address of the machine</small></p>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 text-end">
<label for="port" class="form-label">port</label>
<span class="text-danger" style="margin-right: -12px;">*</span>
</div>
<div class="col-md-8">
<input class="form-control" type="text" name="port" value="" required />
<p class="text-start"><small>port on which the Jilo Agent is listening</small></p>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 text-end">
<label for="name" class="form-label">name</label>
</div>
<div class="col-md-8">
<input class="form-control" type="text" name="name" value="" />
<p class="text-start"><small>description or name of the host (optional)</small></p>
</div>
</div>
<input type="hidden" name="platform" value="<?= htmlspecialchars($platformDetails[0]['id'])?>" />
<input type="hidden" name="item" value="host" />
<input type="hidden" name="new" value="true" />
<br />
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_id) ?>&host=<?= htmlspecialchars($host) ?>#platform<?= htmlspecialchars($platform_id) ?>host<?= htmlspecialchars($host) ?>" />Cancel</a>
&nbsp;&nbsp;
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
</form>
</div>
</div>
<!-- /widget "hosts" -->

View File

@ -0,0 +1,32 @@
<!-- widget "hosts" -->
<div class="card text-center w-50 mx-lef">
<p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p>
<div class="card-body">
<p class="card-text">delete a host:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=host">
<?php
foreach ($hostDetails[0] as $key => $value) {
?>
<div class="row mb-3">
<div class="col-md-4 text-end">
<label for="<?= htmlspecialchars($key) ?>" class="form-label"><?= htmlspecialchars($key) ?>:</label>
</div>
<div class="col-md-8">
<div class="text-start"><?= htmlspecialchars($value ?? '') ?></div>
<input type="hidden" name="<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value ?? '') ?>" />
</div>
</div>
<?php } ?>
<br />
<input type="hidden" name="host" value="<?= htmlspecialchars($hostDetails[0]['id']) ?>" />
<input type="hidden" name="delete" value="true" />
<p class="h5 text-danger">Are you sure you want to delete this host?</p>
<br />
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_id) ?>&host=<?= htmlspecialchars($host) ?>#platform<?= htmlspecialchars($platform_id) ?>host<?= htmlspecialchars($host) ?>" />Cancel</a>
&nbsp;&nbsp;
<input type="submit" class="btn btn-danger btn-sm" value="Delete" />
</form>
</div>
</div>
<!-- /widget "hosts" -->

View File

@ -0,0 +1,52 @@
<!-- widget "hosts" -->
<div class="card text-center w-50 mx-lef">
<p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p>
<div class="card-body">
<p class="card-text">edit host details:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=host">
<div class="row mb-3">
<div class="col-md-4 text-end">
<label for="address" class="form-label">address</label>
<span class="text-danger" style="margin-right: -12px;">*</span>
</div>
<div class="col-md-8">
<input class="form-control" type="text" name="address" value="<?= htmlspecialchars($hostDetails[0]['address'] ?? '') ?>" required autofocus />
<p class="text-start"><small>DNS name or IP address of the machine</small></p>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 text-end">
<label for="port" class="form-label">port</label>
<span class="text-danger" style="margin-right: -12px;">*</span>
</div>
<div class="col-md-8">
<input class="form-control" type="text" name="port" value="<?= htmlspecialchars($hostDetails[0]['port'] ?? '') ?>" required />
<p class="text-start"><small>port on which the Jilo Agent is listening</small></p>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 text-end">
<label for="name" class="form-label">name</label>
</div>
<div class="col-md-8">
<input class="form-control" type="text" name="name" value="<?= htmlspecialchars($hostDetails[0]['name'] ?? '') ?>" />
<p class="text-start"><small>description or name of the host (optional)</small></p>
</div>
</div>
<input type="hidden" name="platform" value="<?= htmlspecialchars($platform_id) ?>" />
<input type="hidden" name="item" value="host" />
<input type="hidden" name="host" value="<?= htmlspecialchars($hostDetails[0]['id']) ?>" />
<br />
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_id) ?>&host=<?= htmlspecialchars($host) ?>#platform<?= htmlspecialchars($platform_id) ?>host<?= htmlspecialchars($host) ?>" />Cancel</a>
&nbsp;&nbsp;
<input type="submit" class="btn btn-primary btn-sm" value="Save" />
</form>
</div>
</div>
<!-- /widget "hosts" -->

View File

@ -0,0 +1,39 @@
<!-- widget "hosts" -->
<div class="card text-center w-75 mx-lef">
<p class="h4 card-header">Jilo configuration :: Jitsi Meet hosts</p>
<div class="card-body">
<p class="card-text">Jitsi hosts configuration
</p>
<?php foreach ($platformsAll as $platform_array) {
$hosts = $hostObject->getHostDetails($platform_array['id']);
?>
<a name="platform<?= htmlspecialchars($platform_array['id']) ?>"></a>
<div class="row mb-1 border <?= isset($_REQUEST['platform']) && (int)$platform_array['id'] === (int)$_REQUEST['platform'] ? 'rounded bg-light' : '' ?>" style="padding: 20px; padding-bottom: 0px;">
<p class="text-start">
platform <strong><?= htmlspecialchars($platform_array['name']) ?></strong>
</p>
<ul class="text-start" style="padding-left: 50px;">
<?php foreach ($hosts as $host_array) { ?>
<li style="padding-bottom: 10px;">
<a name="platform<?= htmlspecialchars($platform_array['id']) ?>host<?= htmlspecialchars($host_array['id']) ?>"></a>
<span class="<?= isset($_REQUEST['platform']) && (int)$platform_array['id'] === (int)$_REQUEST['platform'] && isset($_REQUEST['host']) && (int)$host_array['id'] === (int)$_REQUEST['host'] ? 'border rounded bg-light' : '' ?>" style="padding: 10px;">
<?= htmlspecialchars($host_array['address']) ?>:<?= htmlspecialchars($host_array['port']) ?>
&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,7 +1,14 @@
<!-- widget "config" -->
<div class="card text-center w-75 mx-lef">
<p class="h4 card-header">Jilo web configuration</p>
<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
@ -97,6 +104,16 @@ echo "\n";
<?= 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>

View File

@ -1,10 +1,10 @@
<!-- widget "config" -->
<div class="card text-center w-50 mx-auto">
<!-- 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">
<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">
@ -42,9 +42,10 @@
<input type="hidden" name="new" value="true" />
<br />
<a class="btn btn-secondary" href="<?= htmlspecialchars($app_root) ?>?page=config" />Cancel</a>
<input type="submit" class="btn btn-primary" value="Save" />
<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 "config" -->
<!-- /widget "platforms" -->

View File

@ -1,9 +1,8 @@
<!-- widget "config" -->
<div class="card text-center w-50 mx-auto">
<p class="h4 card-header">Jilo web configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p>
<!-- 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">
<p class="card-text">delete a platform:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config">
<?php
foreach ($platformDetails[0] as $key => $value) {
@ -24,9 +23,10 @@ foreach ($platformDetails[0] as $key => $value) {
<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-secondary" href="<?= htmlspecialchars($app_root) ?>?page=config#platform<?= htmlspecialchars($platform_id) ?>" />Cancel</a>
<input type="submit" class="btn btn-danger" value="Delete" />
<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 "config" -->
<!-- /widget "platforms" -->

View File

@ -1,10 +1,9 @@
<!-- widget "config" -->
<div class="card text-center w-50 mx-auto">
<p class="h4 card-header">Jilo web configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p>
<!-- 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">
<p class="card-text">edit the platform details:</p>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config">
<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;
@ -28,9 +27,10 @@ foreach ($platformDetails[0] as $key => $value) {
<?php } ?>
<br />
<input type="hidden" name="platform" value="<?= htmlspecialchars($platform_id) ?>" />
<a class="btn btn-secondary" href="<?= htmlspecialchars($app_root) ?>?page=config#platform<?= htmlspecialchars($platform_id) ?>" />Cancel</a>
<input type="submit" class="btn btn-primary" value="Save" />
<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 "config" -->
<!-- /widget "platforms" -->

View File

@ -0,0 +1,60 @@
<!-- widget "platforms" -->
<div class="card text-center w-75 mx-lef">
<p class="h4 card-header">Jilo configuration :: Jitsi Meet platforms</p>
<div class="card-body">
<p class="card-text">Jitsi platforms configuration &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

@ -1,5 +1,5 @@
<!-- widget "config" -->
<!-- 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">
@ -7,9 +7,9 @@
<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=config&item=configjs">view only active lines</a></span>
<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=config&item=configjs&mode=raw">view raw file contents</a></span>
<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">
@ -19,4 +19,4 @@ echo htmlspecialchars($platformConfigjs);
</pre>
</div>
</div>
<!-- /widget "config" -->
<!-- /widget "config.js" -->

View File

@ -1,5 +1,5 @@
<!-- widget "config" -->
<!-- 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">
@ -7,9 +7,9 @@
<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=config&item=interfaceconfigjs">view only active lines</a></span>
<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=config&item=interfaceconfigjs&mode=raw">view raw file contents</a></span>
<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">
@ -19,4 +19,4 @@ echo htmlspecialchars($platformInterfaceConfigjs);
</pre>
</div>
</div>
<!-- /widget "config" -->
<!-- /widget "interfaceconfig" -->

View File

@ -1,5 +1,7 @@
<hr /><p class="m-3">NB: This functionality is still under development. The data is just an example.</p><hr /><!-- FIXME remove when implemented -->
<div class="row">
<div class="card w-auto bg-light border-light card-body" style="flex-direction: row;"><?= $widget['title'] ?></div>
</div>
<div class="row">
<div class="card w-auto bg-light border-light card-body filter-results">

View File

@ -38,6 +38,14 @@ It's a multi-user web tool with user levels and access rights integrated into it
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>

View File

@ -1,44 +1,57 @@
<div class="row">
<div class="card w-auto bg-light border-light card-body" style="flex-direction: row;"><?= $widget['title'] ?></div>
<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">
<hr /><p class="m-3">NB: This functionality is still under development. The data is just an example.</p><hr /><!-- FIXME remove when implemented -->
<?php if ($widget['full'] === true) { ?>
<table class="table table-results table-striped table-hover table-bordered">
<thead class="thead-dark">
<tr>
<th scope="col"></th>
<th scope="col">Metric</th>
<?php foreach ($widget['records'] as $record) { ?>
<th scope="col"><?= htmlspecialchars($record['table_headers']) ?></th>
<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>
<tr>
<td>conferences</td>
<?php foreach ($widget['records'] as $record) { ?>
<td><?php if (!empty($record['conferences'])) { ?>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=conferences&from_time=<?= htmlspecialchars($record['from_time']) ?>&until_time=<?= htmlspecialchars($record['until_time']) ?>"><?= htmlspecialchars($record['conferences']) ?></a> <?php } else { ?>
0<?php } ?>
</td>
<?php } ?>
<?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>participants</td>
<?php foreach ($widget['records'] as $record) { ?>
<td><?php if (!empty($record['participants'])) { ?>
<a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=participants&from_time=<?= htmlspecialchars($record['from_time']) ?>&until_time=<?= htmlspecialchars($record['until_time']) ?>"><?= htmlspecialchars($record['participants']) ?></a> <?php } else { ?>
0<?php } ?>
<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 } ?>
<?php } ?>
</tr>
<?php } ?>
<?php } ?>
</tbody>
</table>
<?php } else { ?>
<p class="m-3">No records found.</p>
<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>

View File

@ -18,6 +18,28 @@ $(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
</script>
<script>
// dismissible messages
document.addEventListener('DOMContentLoaded', function() {
// Initialize Bootstrap alerts
var alerts = document.querySelectorAll('.alert');
alerts.forEach(function(alert) {
var closeButton = alert.querySelector('.btn-close');
if (closeButton) {
closeButton.addEventListener('click', function() {
alert.classList.remove('show');
setTimeout(function() {
alert.remove();
}, 150);
});
}
});
});
</script>
</body>
</html>

View File

@ -26,7 +26,7 @@
<?php if ($page === 'agents') { ?>
<script src="<?= htmlspecialchars($app_root) ?>static/agents.js"></script>
<?php } ?>
<?php if ($page === 'graphs') { ?>
<?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>
@ -37,3 +37,13 @@
</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>

View File

@ -1,7 +1,7 @@
<div class="row">
<!-- Sidebar -->
<div class="col-md-3 sidebar-wrapper bg-light" id="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">
<?php
$timeNow = new DateTime('now', new DateTimeZone($userTimezone));
@ -38,28 +38,25 @@ $timeNow = new DateTime('now', new DateTimeZone($userTimezone));
</li>
</a>
<li class="list-group-item bg-light" style="border: none;"><p class="text-end mb-0"><small>graphs</small></p></li>
<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=graphs">
<li class="list-group-item<?php if ($page === 'graphs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<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'; ?>">
<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=latest">
<li class="list-group-item<?php if ($page === 'latest') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<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'; ?>">
<i class="fas fa-list" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="latest data"></i>latest data
</li>
</a>
<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=config&item=configjs">
<li class="list-group-item<?php if ($page === 'config' && $item === 'configjs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<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'; ?>">
<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=config&item=interfaceconfigjs">
<li class="list-group-item<?php if ($page === 'config' && $item === 'interfaceconfigjs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<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'; ?>">
<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>
@ -69,15 +66,49 @@ $timeNow = new DateTime('now', new DateTimeZone($userTimezone));
</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>
<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
</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">
<li class="list-group-item<?php if ($page === 'config' && $item === '') 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
<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
</li>
</a>
<?php } ?>
<?php if ($userObject->hasRight($user_id, 'superuser') ||
$userObject->hasRight($user_id, 'edit whitelist') ||
$userObject->hasRight($user_id, 'edit blacklist') ||
$userObject->hasRight($user_id, 'edit ratelimiting')) { ?>
<a href="<?= htmlspecialchars($app_root) ?>?page=security">
<li class="list-group-item<?php if ($page === 'security') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-shield-alt" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="security"></i>security
</li>
</a>
<?php } ?>
<a href="<?= htmlspecialchars($app_root) ?>?page=status">
<li class="list-group-item<?php if ($page === 'status' && $item === '') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">
<i class="fas fa-heartbeat" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="status"></i>status
</li>
</a>
<?php if ($userObject->hasRight($user_id, 'view app logs')) {?>
<a href="<?= htmlspecialchars($app_root) ?>?page=logs">
<li class="list-group-item<?php if ($page === 'logs') echo ' list-group-item-secondary'; else echo ' list-group-item-action'; ?>">

View File

@ -0,0 +1,223 @@
<!-- 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>
</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>
</div>
</div>
</div>
</div>
<?php } ?>
</div>
<!-- /Security Settings -->

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
INSERT INTO jilo_agent_types VALUES(1,'jvb','/jvb');
INSERT INTO jilo_agent_types VALUES(2,'jicofo','/jicofo');
INSERT INTO jilo_agent_types VALUES(3,'prosody','/prosody');
INSERT INTO jilo_agent_types VALUES(4,'nginx','/nginx');
INSERT INTO jilo_agent_types VALUES(5,'jibri','/jibri');

View File

@ -1,13 +0,0 @@
INSERT INTO rights VALUES(1,'superuser');
INSERT INTO rights VALUES(2,'edit users');
INSERT INTO rights VALUES(3,'view config file');
INSERT INTO rights VALUES(4,'edit config file');
INSERT INTO rights VALUES(5,'view own profile');
INSERT INTO rights VALUES(6,'edit own profile');
INSERT INTO rights VALUES(7,'view all profiles');
INSERT INTO rights VALUES(8,'edit all profiles');
INSERT INTO rights VALUES(9,'view app logs');
INSERT INTO rights VALUES(10,'view all platforms');
INSERT INTO rights VALUES(11,'edit all platforms');
INSERT INTO rights VALUES(12,'view all agents');
INSERT INTO rights VALUES(13,'edit all agents');

View File

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

View File

@ -0,0 +1,27 @@
INSERT INTO users VALUES(1,'demo','$2y$10$tLCLvgYu91gf/zBoc58Am.iVls/SOMcIXO3ykGfgFFei9yneZTrb2');
INSERT INTO users VALUES(2,'demo1','$2y$10$LtV9m.rMCJ.K/g45e6tzDexZ8C/9xxu3qFCkvz92pUYa7Jg06np0i');
INSERT INTO users_meta VALUES(1,1,'demo admin user','admin@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
INSERT INTO users_meta VALUES(2,2,'demo user','demo@example.com',NULL,NULL,'This is a demo user of the demo install of Jilo Web');
INSERT INTO platforms VALUES(1,'meet.lindeas.com','https://meet.lindeas.com','../jilo-meet.lindeas.db');
INSERT INTO platforms VALUES(2,'example.com','https://meet.example.com','../jilo.db');
INSERT INTO logs VALUES(1,2,'2024-09-30 09:54:50','user','Logout: User "demo" logged out. IP: 151.237.101.43');
INSERT INTO logs VALUES(2,2,'2024-09-30 09:54:54','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO logs VALUES(3,2,'2024-10-03 16:34:49','user','Logout: User "demo" logged out. IP: 151.237.101.43');
INSERT INTO logs VALUES(4,2,'2024-10-03 16:34:56','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO logs VALUES(5,2,'2024-10-09 11:08:16','user','Logout: User "demo" logged out. IP: 151.237.101.43');
INSERT INTO logs VALUES(6,2,'2024-10-09 11:08:20','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO logs VALUES(7,2,'2024-10-17 16:22:57','user','Logout: User "demo" logged out. IP: 151.237.101.43');
INSERT INTO logs VALUES(8,2,'2024-10-17 16:23:08','user','Login: User "demo" logged in. IP: 151.237.101.43');
INSERT INTO logs VALUES(9,2,'2024-10-18 08:07:25','user','Login: User "demo" logged in. IP: 42.104.201.119');
INSERT INTO jilo_agents VALUES(1,1,1,'https://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO jilo_agents VALUES(4,1,2,'https://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO jilo_agents VALUES(7,1,3,'http://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO jilo_agents VALUES(8,1,4,'http://meet.lindeas.com:8081','mysecretkey',5);
INSERT INTO hosts VALUES(1,'meet.lindeas.com',8888,2,'main machine');
INSERT INTO hosts VALUES(2,'meet.example.com',9191,2,'test');

View File

@ -1,5 +1,5 @@
Package: jilo-web
Version: 0.2.1
Version: 0.3
Section: web
Priority: optional
Architecture: all

View File

@ -1,4 +1,4 @@
.TH JILO-WEB "8" "October 2024" "jilo-web 0.2.1"
.TH JILO-WEB "8" "January 2025" "jilo-web 0.3"
.SH NAME
jilo-web \- PHP frontent to jilo (jitsi logs observer) database.
.SH DESCRIPTION
@ -17,7 +17,7 @@ Written and maintained by Yasen Pramatarov <yasen@lindeas.com>
https://lindeas.com/jilo
.SH VERSION
0.2.1
0.3
.SH SEE ALSO
jilo(8), jilo-cli(8)

View File

@ -1,5 +1,5 @@
Name: jilo-web
Version: 0.2.1
Version: 0.3
Release: 1%{?dist}
Summary: Jitsi logs web observer
@ -54,6 +54,8 @@ cp %{sourcedir}/man-jilo.8 %{buildroot}/usr/share/man/man8/%{name}.8
/usr/share/man/man8/%{name}.8.gz
%changelog
* Wed Jan 15 2025 Yasen Pramatarov <yasen@lindeas.com> 0.3
- Build of upstream v0.3
* Thu Oct 17 2024 Yasen Pramatarov <yasen@lindeas.com> 0.2.1
- Build of upstream v0.2.1
* Sat Aug 31 2024 Yasen Pramatarov <yasen@lindeas.com> 0.2

View File

@ -7,8 +7,8 @@
* Author: Yasen Pramatarov
* License: GPLv2
* Project URL: https://lindeas.com/jilo
* Year: 2024
* Version: 0.2.1
* Year: 2024-2025
* Version: 0.3
*/
// we start output buffering and.
@ -16,9 +16,15 @@
ob_start();
// sanitize all input vars that may end up in URLs or forms
require '../app/helpers/sanitize.php';
require '../app/includes/sanitize.php';
require '../app/helpers/errors.php';
// Initialize message system
require_once '../app/classes/messages.php';
$messages = [];
//include '../app/includes/messages.php';
require '../app/includes/errors.php';
// error reporting, comment out in production
ini_set('display_errors', 1);
@ -34,14 +40,17 @@ $allowed_urls = [
'participants',
'components',
'graphs',
'data',
'latest',
'agents',
'profile',
'config',
'status',
'logs',
'security',
'help',
'login',
@ -88,14 +97,27 @@ if (isset($_COOKIE['username'])) {
// redirect to login
if ( !isset($_COOKIE['username']) && ($page !== 'login' && $page !== 'register') ) {
header('Location: index.php?page=login');
header('Location: ' . htmlspecialchars($app_root) . '?page=login');
exit();
}
// connect to db of Jilo Web
require '../app/classes/database.php';
require '../app/helpers/database.php';
$dbWeb = connectDB($config);
require '../app/includes/database.php';
try {
$response = connectDB($config);
if (!$response['db']) {
throw new Exception('Could not connect to database: ' . $response['error']);
}
$dbWeb = $response['db'];
} catch (Exception $e) {
Messages::flash('ERROR', 'DEFAULT', getError('Error connecting to the database.', $e->getMessage()));
include '../app/templates/page-header.php';
include '../app/includes/messages.php';
include '../app/includes/messages-show.php';
include '../app/templates/page-footer.php';
exit();
}
// start logging
require '../app/classes/log.php';
@ -103,6 +125,9 @@ include '../app/helpers/logs.php';
$logObject = new Log($dbWeb);
$user_IP = getUserIP();
// init rate limiter
require '../app/classes/ratelimiter.php';
// get platforms details
require '../app/classes/platform.php';
$platformObject = new Platform($dbWeb);
@ -128,29 +153,49 @@ if ($page == 'logout') {
session_destroy();
setcookie('username', "", time() - 100, $config['folder'], $config['domain'], isset($_SERVER['HTTPS']), true);
$notice = "You were logged out.<br />You can log in again.";
// Log successful logout
$user_id = $userObject->getUserId($currentUser)[0]['id'];
$logObject->insertLog($user_id, "Logout: User \"$currentUser\" logged out. IP: $user_IP", 'user');
// Set success message
Messages::flash('LOGIN', 'LOGOUT_SUCCESS');
include '../app/templates/page-header.php';
include '../app/templates/page-menu.php';
include '../app/templates/block-message.php';
include '../app/pages/login.php';
} else {
// if user is logged in, we need user details and rights
if (isset($currentUser)) {
// If by error a logged in user requests the login page
if ($page === 'login') {
header('Location: ' . htmlspecialchars($app_root));
exit();
}
$user_id = $userObject->getUserId($currentUser)[0]['id'];
$userDetails = $userObject->getUserDetails($user_id);
$userRights = $userObject->getUserRights($user_id);
$userTimezone = isset($userDetails[0]['timezone']) ? $userDetails[0]['timezone'] : 'UTC'; // Default to UTC if no timezone is set
// check if the Jilo Server is running
require '../app/classes/server.php';
$serverObject = new Server($dbWeb);
$server_host = '127.0.0.1';
$server_port = '8080';
$server_endpoint = '/health';
$server_status = $serverObject->getServerStatus($server_host, $server_port, $server_endpoint);
if (!$server_status) {
Messages::flash('ERROR', 'DEFAULT', 'The Jilo Server is not running. Some data may be old and incorrect.', false, true);
}
}
// page building
include '../app/templates/page-header.php';
include '../app/templates/page-menu.php';
include '../app/templates/block-message.php';
if (isset($currentUser)) {
include '../app/templates/page-sidebar.php';
}

View File

@ -0,0 +1,3 @@
<?php
phpinfo();
?>

View File

@ -58,24 +58,27 @@ function fetchData(agent_id, url, endpoint, jwtToken, force = false) {
// show the result in the html
resultElement.innerHTML = JSON.stringify(result, null, 2);
// get the cache timestamp from the session
const now = Date.now();
const cacheTimestamp = new Date(now);
// we don't cache the /status
if (endpoint !== '/status') {
// get the cache timestamp from the session
const now = Date.now();
const cacheTimestamp = new Date(now);
// display the cache retrieval date and time
const formattedDate = cacheTimestamp.toLocaleDateString();
const formattedTime = cacheTimestamp.toLocaleTimeString();
cacheInfoElement.style.display = '';
cacheInfoElement.innerHTML = `cache refreshed on ${formattedDate} at ${formattedTime}`;
// display the cache retrieval date and time
const formattedDate = cacheTimestamp.toLocaleDateString();
const formattedTime = cacheTimestamp.toLocaleTimeString();
cacheInfoElement.style.display = '';
cacheInfoElement.innerHTML = `cache refreshed on ${formattedDate} at ${formattedTime}`;
// show the cache buttons
loadCacheButton.disabled = false;
loadCacheButton.style.display = '';
clearCacheButton.disabled = false;
clearCacheButton.style.display = '';
// show the cache buttons
loadCacheButton.disabled = false;
loadCacheButton.style.display = '';
clearCacheButton.disabled = false;
clearCacheButton.style.display = '';
// send the result to PHP to store in session
saveResultToSession(result, agent_id);
// send the result to PHP to store in session
saveResultToSession(result, agent_id);
}
}
} catch (e) {
// Display the error

View File

@ -5,6 +5,7 @@ html, body {
padding: 0;
}
/* menu */
.menu-container {
display: flex;
justify-content: space-between;
@ -43,6 +44,7 @@ html, body {
.menu-left li a:hover, .menu-right li a:hover {
background-color: #111;
}
/* /menu */
.error {
color: red;
@ -140,6 +142,7 @@ html, body {
.main-content {
flex-grow: 1;
transition: width 0.5s ease;
margin-bottom: 50px;
/* width: 80%;*/
}
.main-content.expanded {
@ -149,6 +152,10 @@ html, body {
width: calc(70% + 3em);
}
.sidebar-content a {
text-decoration: none;
}
.logo {
height: 36px;
margin-top: 10px;
@ -161,20 +168,26 @@ html, body {
background-color: lightseagreen;
}
.sidebar-content a {
text-decoration: none;
input[type="select"]:disabled,
input[type="radio"]:disabled {
background-color: #e0e0e0; /* Light gray background */
color: #999999; /* Gray text */
border-color: #cccccc; /* Lighter gray border */
cursor: not-allowed; /* Shows the 'not-allowed' cursor */
opacity: 0.7; /* Makes it slightly transparent */
}
/* pagination */
.pagination {
font-size: 0.66em;
text-align: center;
display: block;
}
.pagination span {
margin-left: 5px;
margin-right: 5px;
}
/* /pagination */
.th-time {
width: 200px;
@ -200,3 +213,17 @@ html, body {
border: 1px solid gray;
border-radius: 4px;
}
/* messages system */
.alert-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 1.2;
margin-bottom: 0.5rem;
}
.alert-sm .btn-close-sm {
padding: 0.25rem 0.25rem;
margin: -0.125rem -0.125rem -0.125rem auto;
font-size: 0.75rem;
}