Compare commits

...

795 Commits
v0.1 ... main

Author SHA1 Message Date
Yasen Pramatarov 896249f833 Imports classes in core code with "use" instead of full references. 2026-05-04 12:25:03 +03:00
Yasen Pramatarov cfed37ef8f Imports classes in plugins with "use" instead of full references. 2026-05-04 10:19:27 +03:00
Yasen Pramatarov 1cb618d043 Init middleware pipeline in case we need to add/reorder sth in plugins 2026-05-02 21:46:30 +03:00
Yasen Pramatarov e7b32da062 Adds a fast plugin DB guard in bootstrap, to ensure tables existence. 2026-04-29 21:05:24 +03:00
Yasen Pramatarov e2284695fc Validates table names when purging 2026-04-11 21:41:54 +03:00
Yasen Pramatarov 665d5bded9 troubleshooting 2026-04-11 19:15:09 +03:00
Yasen Pramatarov 598458f53c adds docs 2026-04-11 19:14:26 +03:00
Yasen Pramatarov 8dc0b526ab Centralizes url canonicalization and normalization 2026-04-11 19:14:19 +03:00
Yasen Pramatarov b88b2ce8a5 Purging a plugin looks for and removes its tables 2026-04-11 19:11:30 +03:00
Yasen Pramatarov 10460baad8 Adds option for dots in the profile menu 2026-03-28 10:27:42 +02:00
Yasen Pramatarov 3287311b2d Removes more hardcoded "jilo" strings and fixes the tests 2026-03-25 12:51:18 +02:00
Yasen Pramatarov 0c5a48d851 Removes some hardcoded Jilo app name strings 2026-03-25 11:43:37 +02:00
Yasen Pramatarov b388edbb60 Adds failover for site name var early in index 2026-03-23 17:22:07 +02:00
Yasen Pramatarov 2e5ee5632b Fixes admin links in index 2026-03-23 14:02:38 +02:00
Yasen Pramatarov 5771fb0fe1 Adds option for dot notification in menus and cleans up empty lines 2026-03-23 10:47:26 +02:00
Yasen Pramatarov 2aaf051677 Adds hook to profile page for plugins to display data in. 2026-03-21 15:59:34 +02:00
Yasen Pramatarov 32edadc0f9 Fixes images upload 2026-03-17 13:33:17 +02:00
Yasen Pramatarov 0af09cb4d1 Adds uploads helper in core 2026-03-14 14:37:04 +02:00
Yasen Pramatarov e9dc4463c1 Uses datetime helper to display times 2026-01-29 14:12:36 +02:00
Yasen Pramatarov 32c11a9c04 Removes obsolete calls to old datetime helper 2026-01-29 13:09:56 +02:00
Yasen Pramatarov 5c4a50c68b Moves the datetime helper loading to index 2026-01-29 10:40:33 +02:00
Yasen Pramatarov e29ee23490 Moves admin page to datetime helper 2026-01-28 19:33:21 +02:00
Yasen Pramatarov 79f06f9f04 Adds a centralized datetime helper 2026-01-28 19:30:56 +02:00
Yasen Pramatarov c418337736 Fixes index to allow plugins to have their own allowed/public urls 2026-01-23 13:17:37 +02:00
Yasen Pramatarov 0ef061de56 Fixes tests 2026-01-21 19:56:14 +02:00
Yasen Pramatarov 841f25f28b Changes the db collation to utf8mb4_unicode_ci 2026-01-21 19:55:36 +02:00
Yasen Pramatarov cf10ab021e Adds "settings" and "migrations" to main.sql 2026-01-21 11:47:19 +02:00
Yasen Pramatarov 37e75ff8fa Fixes tests 2026-01-21 10:38:56 +02:00
Yasen Pramatarov 9127f97cc9 Fixes tests 2026-01-20 23:48:47 +02:00
Yasen Pramatarov 3b50d81fb4 Moves to App API 2026-01-20 23:40:44 +02:00
Yasen Pramatarov a030562071 Fixes tests 2026-01-20 23:38:49 +02:00
Yasen Pramatarov 138ea70185 Updates core code to use App(db) 2026-01-20 22:44:47 +02:00
Yasen Pramatarov 71c25c778f Updates plugins descriptions 2026-01-20 15:31:34 +02:00
Yasen Pramatarov 0165896a0b Fixes index to pass the plugin to only load the internal page 2026-01-20 11:46:21 +02:00
Yasen Pramatarov 1dde66a9f1 Fixes logs and register plugins 2026-01-20 11:38:17 +02:00
Yasen Pramatarov eb4b5ca7bc Migrates logs plugin to App API, updates docs 2026-01-20 10:06:39 +02:00
Yasen Pramatarov 58c2651796 Updates register plugin's docs 2026-01-20 10:05:47 +02:00
Yasen Pramatarov 80aaa0cab6 Fixes admin/plugins. Shows tables - owned by plugin and just referenced 2026-01-19 21:28:35 +02:00
Yasen Pramatarov 4c5136adf4 Fixes the register plugin 2026-01-19 20:19:02 +02:00
Yasen Pramatarov d318b621d5 Troubleshoots admin/plugins page 2026-01-19 18:05:42 +02:00
Yasen Pramatarov d3a9d78e2c Updates and fixes the register plugin 2026-01-19 17:59:35 +02:00
Yasen Pramatarov c42b2ff483 Moves plugin loading after we have DB connection in index 2026-01-19 17:55:49 +02:00
Yasen Pramatarov 0b3ff9e40b Debugs admin/plugins. Makes it use App API 2026-01-19 12:08:17 +02:00
Yasen Pramatarov ddd8fdac3a Removes old "enabled" field in json. Plugins state is in DB now. 2026-01-19 11:43:18 +02:00
Yasen Pramatarov e1dc0bbf32 Troubleshoots logs plugin 2026-01-19 11:42:31 +02:00
Yasen Pramatarov aff2ec4003 Debugs admin/plugins feature 2026-01-19 11:40:36 +02:00
Yasen Pramatarov 064614f73f Refactors admin/plugins to use only DB. Adds buttons and modal info page 2026-01-19 11:31:34 +02:00
Yasen Pramatarov 81e665644c Moves index to App API 2026-01-16 12:59:34 +02:00
Yasen Pramatarov d0101925de Adds a helper wrapper to "logs" plugin. 2026-01-14 17:36:49 +02:00
Yasen Pramatarov 521d8eafab Adds plugin route registry, one plugin can register multiple routes 2026-01-13 13:49:56 +02:00
Yasen Pramatarov e2e3d74de1 Integrates App\App into index.php 2026-01-13 12:38:07 +02:00
Yasen Pramatarov 62b2242b75 Adds a simple Core/App unit test 2026-01-13 12:04:13 +02:00
Yasen Pramatarov 1228abe6f2 Adds minimal app services API for plugins 2026-01-13 11:53:31 +02:00
Yasen Pramatarov f4238f205f Moves $timeNow as early as possible. Available to whole site. 2026-01-12 13:13:08 +02:00
Yasen Pramatarov 49ba04bffa Design fixes 2025-12-31 12:53:38 +02:00
Yasen Pramatarov bbceb44c3d Adds session expiration debug message 2025-12-30 12:14:49 +02:00
Yasen Pramatarov d67e376220 Fixes remember_me in login form 2025-12-30 11:43:00 +02:00
Yasen Pramatarov a5a08ec8a0 Debugs tests 2025-12-26 22:33:53 +02:00
Yasen Pramatarov 0bb5fc2dc4 Removes admin-tools page 2025-12-24 09:21:15 +02:00
Yasen Pramatarov a1c585ed05 Updates CHANGELOD 2025-12-23 21:45:06 +02:00
Yasen Pramatarov 2a97539093 Adds plugin management section, part of the admin page 2025-12-23 18:39:07 +02:00
Yasen Pramatarov 1ca0515ee1 Updates CHANGELOG 2025-12-23 17:39:13 +02:00
Yasen Pramatarov 5d62380c8b Adds "admin" page for all admin tasks 2025-12-23 16:47:37 +02:00
Yasen Pramatarov b609aca2cc Loads email templates from "emails" views, available to plugins too. 2025-12-23 14:34:11 +02:00
Yasen Pramatarov 20cc575792 Updates index.php to use global APP_PATH var 2025-12-23 13:26:23 +02:00
Yasen Pramatarov 4639baeef9 Adds "admin" dashboard page 2025-12-21 11:05:39 +02:00
Yasen Pramatarov a272294fc0 Encodes correctly the login regirect URL parameters 2025-12-15 18:27:47 +02:00
Yasen Pramatarov b239b73689 Fixes public pages that are also authenticated pages. 2025-12-15 17:58:42 +02:00
Yasen Pramatarov 7031acd46d Replaces error_log with app_log in 2FA 2025-12-15 17:53:35 +02:00
Yasen Pramatarov c1d71fba77 Adds logger helper to index 2025-12-15 17:51:51 +02:00
Yasen Pramatarov 167bb2c075 Enhances logger helper with fallback if there is no log plugin 2025-12-15 17:36:30 +02:00
Yasen Pramatarov dbd0ab5f0e Adds password reset email template 2025-12-14 17:22:48 +02:00
Yasen Pramatarov cfefb8cc56 Adds email helper for sending emails 2025-12-14 17:21:48 +02:00
Yasen Pramatarov 31bc4d60e4 Switches profile edit to action-card design 2025-12-14 16:43:51 +02:00
Yasen Pramatarov 817782a766 Fixes profile avatar uploads 2025-12-14 16:43:26 +02:00
Yasen Pramatarov 8280f66b6d Changes credentials pages to action-card. Deletes unused credentials.php 2025-12-14 16:10:15 +02:00
Yasen Pramatarov 19521b432d Changes action pages to uniform action-card design 2025-12-14 15:40:30 +02:00
Yasen Pramatarov 2639a0f60a Fixes collapsing sidebar design 2025-12-14 15:15:29 +02:00
Yasen Pramatarov 9485cd0769 Adds a hook to load plugin assets 2025-11-28 18:44:39 +02:00
Yasen Pramatarov 06ddd768aa Adds plugin asset page 2025-11-28 18:39:48 +02:00
Yasen Pramatarov 25037008de Adds a plugin hook to the profile page 2025-11-27 21:27:51 +02:00
Yasen Pramatarov 9d93106d00 Adds a hook in account menu for plugins 2025-11-27 13:02:51 +02:00
Yasen Pramatarov 1252c421bc Redesigns profile page 2025-11-27 12:44:15 +02:00
Yasen Pramatarov 969875460f Redesigns themes page 2025-11-26 19:28:25 +02:00
Yasen Pramatarov 251cfa35f3 Fixes typo 2025-11-26 19:27:55 +02:00
Yasen Pramatarov 014ef05d05 Fix in register plugin 2025-11-25 12:25:42 +02:00
Yasen Pramatarov 8eae3cf124 Redesigns admin-tools page 2025-11-23 22:48:54 +02:00
Yasen Pramatarov 35def007ca Updates CSS and redesigns pagination 2025-11-23 22:47:20 +02:00
Yasen Pramatarov 82fb01384f Fixes for the db migration routine 2025-11-21 21:09:14 +02:00
Yasen Pramatarov 65a4dc7f18 Introduces Log Throtter to prevent log flooding 2025-11-21 20:58:19 +02:00
Yasen Pramatarov c38e5ef4a6 Refactoring the DB migration and Admin Tools functionality 2025-11-21 20:44:37 +02:00
Yasen Pramatarov 4b330dff6c Adds the option to run next DB migration(s) one by one 2025-11-21 11:03:58 +02:00
Yasen Pramatarov b94a3df731 Stores applied db migrations in the DB to keep track of 2025-11-21 11:00:56 +02:00
Yasen Pramatarov 785e9a84eb Monthly dashboard stats redesign 2025-11-20 12:45:18 +02:00
Yasen Pramatarov f853cf137b Reorganizes dashboard 2025-11-20 12:19:18 +02:00
Yasen Pramatarov bcbffb62aa Adds CSS for dashboard widgets 2025-11-20 12:09:41 +02:00
Yasen Pramatarov 1b01a0a0eb Redesigns credentials/2FA pages 2025-11-19 22:54:16 +02:00
Yasen Pramatarov 5422d63d83 Redesigns profile edit page 2025-11-19 22:28:15 +02:00
Yasen Pramatarov b90d8099c1 Redesigns password-forgot and password-reset pages 2025-11-19 21:54:51 +02:00
Yasen Pramatarov 85ea44c1e3 Redesigns register button 2025-11-19 21:43:30 +02:00
Yasen Pramatarov 67cc2a67e8 Redesigns login page 2025-11-19 19:54:16 +02:00
Yasen Pramatarov 9908f555b2 Fixes validator to accept "0" as valid value 2025-11-19 19:39:00 +02:00
Yasen Pramatarov 76385b78d5 Redesigns the sidebar 2025-11-19 19:32:17 +02:00
Yasen Pramatarov 0f5f7a03e0 Redesigns main elements, menu, and CSS 2025-11-19 19:10:23 +02:00
Yasen Pramatarov b9d883fe41 Prepare for release 0.4.1 2025-11-14 10:05:09 +02:00
Yasen Pramatarov 3cc5a510f5 Fixes migration message 2025-11-14 09:50:24 +02:00
Yasen Pramatarov 6c26b422e5 Minor fixes 2025-11-13 22:19:34 +02:00
Yasen Pramatarov 51a9f75429 Minor fixes 2025-11-13 19:47:53 +02:00
Yasen Pramatarov 619476e799 Fixes test DB migration functionality 2025-11-13 16:37:47 +02:00
Yasen Pramatarov 34613d9748 Empty lines clean-up 2025-11-11 17:55:47 +02:00
Yasen Pramatarov 2ed49eb12d Makes sure feedback messages are included only once 2025-11-11 17:40:08 +02:00
Yasen Pramatarov 26c9f49138 Fixes flash messages to show up only once per page 2025-11-11 13:55:07 +02:00
Yasen Pramatarov 77f5921dff Fixes session tests error 2025-09-29 21:35:57 +03:00
Yasen Pramatarov 29c2ecf40c Fixes github tests workflow 2025-09-29 21:30:38 +03:00
Yasen Pramatarov 656cd3c976 Small bugfixes 2025-09-25 20:48:10 +03:00
Yasen Pramatarov d90320f5f9 Fixes feedback notifications 2025-09-25 18:23:18 +03:00
Yasen Pramatarov 3695761b9e Adds a notice for maintenance mode for superusers 2025-09-25 18:18:40 +03:00
Yasen Pramatarov 692fec9bfe Retries after timeout when in maintenance mode 2025-09-25 18:04:30 +03:00
Yasen Pramatarov 5f9a0fe75b Fixes the migrations feedback notice 2025-09-25 17:26:03 +03:00
Yasen Pramatarov d7b029c255 Adds the ability to have non-sanitized feedback messages 2025-09-25 17:22:24 +03:00
Yasen Pramatarov 77be82d8e6 Integrates highlightjs to SQL view modal 2025-09-25 17:12:11 +03:00
Yasen Pramatarov f79c1765b6 Adds Highlight.js library 2025-09-25 17:11:34 +03:00
Yasen Pramatarov de486ba7e7 Admin tools cleanup and view SQL modal 2025-09-25 12:57:02 +03:00
Yasen Pramatarov eebb815ad1 Adds uniform style to admin-tools template 2025-09-25 11:59:02 +03:00
Yasen Pramatarov 49e147c5b5 Moves imgration flag to DB with fallback to file 2025-09-25 11:50:29 +03:00
Yasen Pramatarov a77cf5b328 Adds simple admin-tools page 2025-09-25 11:37:54 +03:00
Yasen Pramatarov f22fa76987 Fixes db migration code 2025-09-24 21:29:31 +03:00
Yasen Pramatarov 08953c6272 Adds initial support for maintenance mode 2025-09-24 20:27:17 +03:00
Yasen Pramatarov 315b68f928 Adds initial support for DB upgrades/migrations 2025-09-24 19:44:38 +03:00
Yasen Pramatarov 056388be71 Makes theme setting per-user 2025-09-24 16:56:14 +03:00
Yasen Pramatarov dfcc1dc7d8 Auto-generates themes config if missing 2025-09-12 11:56:55 +03:00
Yasen Pramatarov d71f3f4f62 Fixes theme folders structure 2025-09-12 11:49:55 +03:00
Yasen Pramatarov 3e72568141 Fixes theme helper 2025-06-26 14:48:51 +03:00
Yasen Pramatarov 1993a7e2de Fixes themes footers 2025-06-26 14:48:10 +03:00
Yasen Pramatarov 91cabf56e7 Adds theme-asset page in index 2025-06-26 14:27:54 +03:00
Yasen Pramatarov aea05ce0e6 Fixes theme asset helper 2025-06-26 14:27:15 +03:00
Yasen Pramatarov 85a489244d Adds modern theme screenshot 2025-06-26 14:13:19 +03:00
Yasen Pramatarov 37b65897df Adds retro theme screenshot 2025-06-26 14:10:41 +03:00
Yasen Pramatarov 361a1e6c6d Troubleshoots theme helper 2025-06-23 14:08:11 +03:00
Yasen Pramatarov 51f0b0b369 Fiexs theme view to use the new theme assets code 2025-06-23 14:01:29 +03:00
Yasen Pramatarov 30ea7ff5c0 Removes getScreenshotUrl, we now use generic getAssetUrl for all assets 2025-06-22 13:58:17 +03:00
Yasen Pramatarov 55829faf85 Fixes theme helper to use the new theme assets helper 2025-06-20 13:55:08 +03:00
Yasen Pramatarov 37566b5122 Adds helper to manage all static assets of a theme 2025-06-19 13:47:09 +03:00
Yasen Pramatarov 8203c10f37 Adds theme data to be passed to the views 2025-06-19 13:45:14 +03:00
Yasen Pramatarov b1dae54aac Fixes templates loading in index 2025-06-18 12:34:56 +03:00
Yasen Pramatarov d65b7bcc55 Fixes index to work with latest session and config changes 2025-06-17 12:23:13 +03:00
Yasen Pramatarov a0f3e84432 Fixes theme helper to have config array always available 2025-06-16 12:22:26 +03:00
Yasen Pramatarov c9490cf149 Troubleshoots router class 2025-06-13 12:20:37 +03:00
Yasen Pramatarov ad8c833862 Refactors session, adds random session name if not configured 2025-06-11 12:18:48 +03:00
Yasen Pramatarov 47875289a8 Bugfixes theme switcher 2025-06-10 11:55:06 +03:00
Yasen Pramatarov e544176cdd Bugfixes session class 2025-06-08 11:52:53 +03:00
Yasen Pramatarov 65f9c4da3c Moves session variables to config file 2025-06-06 11:51:09 +03:00
Yasen Pramatarov bdcc308188 Adds CSRF the theme switcher 2025-06-03 11:01:14 +03:00
Yasen Pramatarov 06f8229a8c Bugfixes theme switcher 2025-06-01 10:39:39 +03:00
Yasen Pramatarov 1c93864567 Adds alternative test theme 2025-05-29 10:23:43 +03:00
Yasen Pramatarov 522d84f203 Bugfixes theme changer page 2025-05-28 10:16:12 +03:00
Yasen Pramatarov 6617b3bb28 Updates changelog 2025-05-27 10:10:04 +03:00
Yasen Pramatarov c4800f4943 Adds js to default theme 2025-05-26 10:08:46 +03:00
Yasen Pramatarov f0820b05c2 Adds CSS to default theme 2025-05-25 10:07:27 +03:00
Yasen Pramatarov 1b2ff95c1e Adds theme item in profile menu 2025-05-24 09:59:56 +03:00
Yasen Pramatarov 4867df89a1 Bugfixes theme helper 2025-05-23 09:58:50 +03:00
Yasen Pramatarov 4715a26af7 Initialize themes system 2025-05-22 15:13:23 +03:00
Yasen Pramatarov d366c1dd10 Makes index dynamically load theme's templates 2025-05-22 14:39:20 +03:00
Yasen Pramatarov 2d0c280a0a Adds theme switcher controller and view 2025-05-22 14:33:54 +03:00
Yasen Pramatarov c2cfd503ee Adds theme-related feedback messages 2025-05-22 14:20:36 +03:00
Yasen Pramatarov 6c8806965e Adds header and footer for the example "modern" theme 2025-05-20 15:20:28 +03:00
Yasen Pramatarov 8745c0598f Adds an example theme "modern" 2025-05-19 15:05:07 +03:00
Yasen Pramatarov 69b40ca560 Adds theme config, prepares for themes system 2025-05-18 15:01:05 +03:00
Yasen Pramatarov c53591e9ec Adds theme helper, prepares for themes system 2025-05-15 14:54:21 +03:00
Yasen Pramatarov 24c844db49 Allows superuser to load settigs page 2025-05-08 20:07:53 +03:00
Yasen Pramatarov 65f0758e82 Updates logs plugin to new pages system, bumps version 2025-05-08 19:38:52 +03:00
Yasen Pramatarov 2f4b0b7aef Updates register plugin to new pages system, bumps version 2025-05-08 19:38:21 +03:00
Yasen Pramatarov 36e81104f1 Refactors index.php to allow multiple pages per plugin 2025-05-08 19:37:35 +03:00
Yasen Pramatarov fd835dd058 Fixes log levels displaying 2025-04-27 21:38:24 +03:00
Yasen Pramatarov d886bcf755 Adds proper log levels to log plugin 2025-04-27 21:20:14 +03:00
Yasen Pramatarov 81b4187ae8 Removes obsolete insertLog() 2025-04-27 19:21:23 +03:00
Yasen Pramatarov bbccb54059 Updates log calls to new syntax 2025-04-27 19:00:58 +03:00
Yasen Pramatarov 1e975f7b18 Makes old code work with the new Log plugin 2025-04-27 15:55:35 +03:00
Yasen Pramatarov 457c946946 Adds some user right restrictions 2025-04-27 15:48:07 +03:00
Yasen Pramatarov f84a337607 Fixes log plugin 2025-04-27 15:43:45 +03:00
Yasen Pramatarov fa3e75f722 Fixes log plugin 2025-04-27 15:41:01 +03:00
Yasen Pramatarov 1f3d331b25 Adds pipeline in index for all middleware 2025-04-26 15:36:41 +03:00
Yasen Pramatarov 315fbcb18f Updates github workflow after mariadb migration 2025-04-25 18:42:28 +03:00
Yasen Pramatarov 6fdf123f9f Fixes tests 2025-04-25 18:30:24 +03:00
Yasen Pramatarov 4a43d8cfc7 Fixes tests 2025-04-25 17:15:56 +03:00
Yasen Pramatarov adb8e42d61 Changes sql tables to use singular names 2025-04-25 16:16:38 +03:00
Yasen Pramatarov 880c45025c Migrates app database from SQLite to MariaDB 2025-04-25 12:10:29 +03:00
Yasen Pramatarov 630f71ce4d Fixes SQL syntax typos 2025-04-25 11:22:20 +03:00
Yasen Pramatarov e8576d3e94 Removes logging logic from index and replaces old log class with a wrapper 2025-04-25 10:20:57 +03:00
Yasen Pramatarov ff28ebf753 Adds log entry to main menu 2025-04-25 10:14:21 +03:00
Yasen Pramatarov 242b63317b Makes the old code work with the new Log plugin 2025-04-25 10:13:12 +03:00
Yasen Pramatarov a004602ce2 Replaces olg logs helper with IP helper 2025-04-25 10:07:01 +03:00
Yasen Pramatarov c749726a79 Removes old log pages 2025-04-25 10:05:51 +03:00
Yasen Pramatarov 761c27c0d3 Adds Log plugin 2025-04-25 10:02:49 +03:00
Yasen Pramatarov fe91a91081 Adds NullLogger when logging system is missing 2025-04-25 09:52:48 +03:00
Yasen Pramatarov 0447439f99 Adds SQL file(preparation for sqlite->mariadb migration) 2025-04-25 09:48:43 +03:00
Yasen Pramatarov ed1c305358 Fixes config code 2025-04-24 14:30:35 +03:00
Yasen Pramatarov 13f2ca4fe4 Moves database connection to DatabaseConnector core class 2025-04-24 14:12:24 +03:00
Yasen Pramatarov 891e85b0bb Adds DatabaseConnector core class 2025-04-24 13:57:08 +03:00
Yasen Pramatarov c2f0fe6793 Moves config loading to ConfigLoader core class 2025-04-24 13:52:37 +03:00
Yasen Pramatarov 7dfbe49996 Adds ConfigLoader core class 2025-04-24 13:49:52 +03:00
Yasen Pramatarov ed0baf18d3 Moves plugin disovery and hooks code away from index 2025-04-24 13:32:45 +03:00
Yasen Pramatarov 8628985361 Adds HookDispatcher core class for plugin hooks 2025-04-24 13:25:30 +03:00
Yasen Pramatarov facddb0d6d Adds PluginManager core class 2025-04-24 13:25:03 +03:00
Yasen Pramatarov 9797caa58e Moves session auth logic to the new core/Router class 2025-04-24 12:37:59 +03:00
Yasen Pramatarov 9c896d9e0e Creates /app/core/ for core infrastructure code, essential for the app 2025-04-24 12:34:27 +03:00
Yasen Pramatarov bccd48014b Removes no more used session middleware 2025-04-23 13:58:43 +03:00
Yasen Pramatarov 10083ff7af Adds 'logout' and 'register' to login redirect whitelist 2025-04-23 13:57:11 +03:00
Yasen Pramatarov cfa8540be9 Removes username forms autofocus 2025-04-23 13:56:07 +03:00
Yasen Pramatarov 3657dd70cf Fixes session timeout and login issues 2025-04-23 13:54:59 +03:00
Yasen Pramatarov e88229bee2 Troubleshoots login redirects 2025-04-22 16:20:56 +03:00
Yasen Pramatarov 0b59072d9b Adds login redirection to original requested page 2025-04-22 15:31:50 +03:00
Yasen Pramatarov 6542df9074 Fixes the tests 2025-04-17 10:59:40 +03:00
Yasen Pramatarov 40c646291e Removes old registration core code 2025-04-17 10:45:29 +03:00
Yasen Pramatarov 4877354e8d Fixes register plugin 2025-04-17 10:41:40 +03:00
Yasen Pramatarov 61d23cd8c2 Lets plugins add themselves to the public pages 2025-04-17 10:36:45 +03:00
Yasen Pramatarov 8dfd54eb9f Replaces hardcoded register link with a plugin hook 2025-04-17 10:31:35 +03:00
Yasen Pramatarov af8d86321f Removes hardcoded "register" page 2025-04-17 10:30:34 +03:00
Yasen Pramatarov 26817c1bb6 Adds registration plugin 2025-04-17 10:29:31 +03:00
Yasen Pramatarov 6443eb9b00 Makes plugin system plugin-name agnostic 2025-04-17 10:20:37 +03:00
Yasen Pramatarov 14eefb99e9 Adds "manage plugins" right 2025-04-17 09:46:19 +03:00
Yasen Pramatarov 3915ca6633 Prepares for plugins. Autodiscovery and hooks. 2025-04-16 20:23:27 +03:00
Yasen Pramatarov 5246c47ee6 Makes csrf_token a global constant and moves it to includes 2025-04-16 13:11:51 +03:00
Yasen Pramatarov 221a6e8139 Removes system settings entries from sidebar menu 2025-04-15 22:43:23 +03:00
Yasen Pramatarov b098096930 Uses $userId instead of session var 2025-04-15 22:40:29 +03:00
Yasen Pramatarov 47779baa5e Adds top right system menu 2025-04-15 22:37:49 +03:00
Yasen Pramatarov eebdbc409c Adds top right help menu 2025-04-15 22:29:55 +03:00
Yasen Pramatarov 95530ed5f0 Adds CSRF to profile edit pages 2025-04-15 18:10:17 +03:00
Yasen Pramatarov 0a7f3737c5 Explicitly adds/removes rights, makes possible to remove all rights 2025-04-15 18:05:09 +03:00
Yasen Pramatarov 9cb7812144 Bugfixes 2025-04-15 17:57:13 +03:00
Yasen Pramatarov 4625321079 Removes length check for old password 2025-04-14 19:39:51 +03:00
Yasen Pramatarov 1c2c1a76fa Fixes bugs in login ratelimiting 2025-04-14 19:36:07 +03:00
Yasen Pramatarov 8d64bf7c6e Ratelimits only failed login attempts 2025-04-14 19:12:26 +03:00
Yasen Pramatarov 45181c11c5 Fixes db connection issues 2025-04-14 18:07:15 +03:00
Yasen Pramatarov e96480807c Updates SQL schemas 2025-04-14 18:06:44 +03:00
Yasen Pramatarov 9e94639657 Makes password at least 8 chars 2025-04-14 15:48:54 +03:00
Yasen Pramatarov 649a94c560 Fixes to show session expiration only once 2025-04-14 15:31:19 +03:00
Yasen Pramatarov 8655258ac3 Standartizes $userId as user ID variable in whole app 2025-04-14 10:39:58 +03:00
Yasen Pramatarov 67ba6b38c7 Session expiration bug fix 2025-04-14 10:06:13 +03:00
Yasen Pramatarov 16854f0f77 Fixes tests and adds Session unit test 2025-04-13 20:51:52 +03:00
Yasen Pramatarov 582b5492fe Removes unneded login reirects 2025-04-13 20:05:10 +03:00
Yasen Pramatarov 101f4c539a Validates pagination vars 2025-04-13 19:49:47 +03:00
Yasen Pramatarov 522cded113 Implements the session class 2025-04-13 19:46:48 +03:00
Yasen Pramatarov f77e15bf44 Implements the new session class 2025-04-13 19:34:13 +03:00
Yasen Pramatarov dbdbe1bf49 Switches to session class in templates 2025-04-13 19:12:28 +03:00
Yasen Pramatarov d3f0c90272 Removes code duplicating with session class 2025-04-13 19:11:52 +03:00
Yasen Pramatarov 566b16190e Adds session timeout message 2025-04-13 19:06:48 +03:00
Yasen Pramatarov 5281102e36 Adds a special 'session' class for all session things. 2025-04-13 15:18:53 +03:00
Yasen Pramatarov b6420391e1 Prepares for v0.4 2025-04-12 17:21:34 +03:00
Yasen Pramatarov f8118315e7 Fixes session tests 2025-04-12 16:48:53 +03:00
Yasen Pramatarov d28d69d350 Fixes CSRF issue after login with 2fa code 2025-04-12 16:28:33 +03:00
Yasen Pramatarov 2ca1714992 Fixes session management and increases time to 2 hours w/out "remember me" 2025-04-12 16:22:41 +03:00
Yasen Pramatarov d72dd5fabc Fixes config editing 2025-04-11 18:29:47 +03:00
Yasen Pramatarov d253d87515 Fixes config file editing 2025-04-11 16:55:08 +03:00
Yasen Pramatarov 9d3bb9ef04 Fixes border case of empty profile fields 2025-04-09 09:17:17 +03:00
Yasen Pramatarov f27f3fe62f Fixes tests 2025-04-08 13:13:49 +03:00
Yasen Pramatarov 0d4251b321 Adds forgotten password reset functionality 2025-04-08 12:12:14 +03:00
Yasen Pramatarov 11fa58bd6e Adds site name to the config 2025-04-08 12:02:01 +03:00
Yasen Pramatarov 71b0448004 Adds 2fa to the login page 2025-04-08 10:30:18 +03:00
Yasen Pramatarov eb0a603b8d Updates credentials page and session vars 2025-04-08 10:30:07 +03:00
Yasen Pramatarov 947a4e39c5 Updates 2fa templates 2025-04-08 10:29:21 +03:00
Yasen Pramatarov 7b7e44faf2 Fixes 2fa classes 2025-04-08 10:27:52 +03:00
Yasen Pramatarov ac1581e8de Fix for blank profile fields 2025-04-08 10:24:43 +03:00
Yasen Pramatarov e00599b4f0 Fixes typo 2025-04-07 16:41:22 +03:00
Yasen Pramatarov 200f87ea48 Adds right profile dropdown menu 2025-04-07 16:35:05 +03:00
Yasen Pramatarov 7676bcd1c1 Updates user class for cdentials management 2025-04-07 16:28:56 +03:00
Yasen Pramatarov 925df9b915 Adds credentials page 2025-04-07 16:21:35 +03:00
Yasen Pramatarov 7668ee2040 Adds CSRF protection to profile page 2025-04-07 15:24:53 +03:00
Yasen Pramatarov 90688016e8 Adds class and SQL for two-factor auth 2025-04-07 12:44:22 +03:00
Yasen Pramatarov b4fabb6d59 Fixes border case when timezone is empty but not null 2025-04-07 12:32:54 +03:00
Yasen Pramatarov aa530c20d2 Removes closing php tags 2025-03-17 12:38:24 +02:00
Yasen Pramatarov 69ce646bad Updates changelog 2025-03-03 21:44:35 +02:00
Yasen Pramatarov 08c20fa2b9 Updates changelog 2025-03-03 21:43:47 +02:00
Yasen Pramatarov e2daf22ad7 HTML fixes 2025-02-28 13:50:54 +02:00
Yasen Pramatarov 921f310ac1 Adds CSRF toek to registration 2025-02-24 22:05:20 +02:00
Yasen Pramatarov d9bee210d4 Adds 'ip' to validator 2025-02-24 14:47:23 +02:00
Yasen Pramatarov 2fc6940c11 Adds missing feedback messages to login and security 2025-02-24 14:08:05 +02:00
Yasen Pramatarov ecad8e2801 Adds back auto-blacklisting in rate limiter 2025-02-23 19:35:38 +02:00
Yasen Pramatarov 4a18c344c8 Fixes rate limiting bugs 2025-02-23 19:22:47 +02:00
Yasen Pramatarov 58633313e1 Fixes user tests 2025-02-23 18:03:19 +02:00
Yasen Pramatarov 0f6dda44b8 Gets the client IP from a central place 2025-02-23 17:58:26 +02:00
Yasen Pramatarov b4b5a7ac8f Fixes CSRF 2025-02-23 17:48:02 +02:00
Yasen Pramatarov a45e064c18 Fixes registration logging 2025-02-23 17:47:06 +02:00
Yasen Pramatarov ecb4e0fab4 Fixes login and register forms 2025-02-23 17:28:20 +02:00
Yasen Pramatarov 035681ab28 Fixes app root redirection on ratelimiting 2025-02-23 15:21:40 +02:00
Yasen Pramatarov 34779bb891 Adds proper logging to CSRF middleware 2025-02-23 13:51:36 +02:00
Yasen Pramatarov c61f42792f Adds logging to component class and switches to bound params 2025-02-23 13:15:46 +02:00
Yasen Pramatarov 788167e251 Switches settings to use the feedback messaging 2025-02-23 13:14:58 +02:00
Yasen Pramatarov 019f31cc05 Temporary fix for CSRF logging 2025-02-23 00:04:26 +02:00
Yasen Pramatarov 91aca75138 Fixes reload after new platform adding 2025-02-23 00:02:15 +02:00
Yasen Pramatarov 66fb6bf576 Adds CSRF to settings page 2025-02-23 00:01:59 +02:00
Yasen Pramatarov ad6ca25493 Adds CSRF tokens to settings edit page 2025-02-22 18:55:17 +02:00
Yasen Pramatarov 4b4cac7cec Fixes conferences page templates 2025-02-21 12:06:38 +02:00
Yasen Pramatarov 487c23da3e Fixes errors in ratelimiter 2025-02-21 11:44:52 +02:00
Yasen Pramatarov 4182ba6c1b Fixes errors in security page 2025-02-21 11:44:04 +02:00
Yasen Pramatarov 20094b5e42 Reserves test folders Functional and Utils 2025-02-20 10:46:35 +02:00
Yasen Pramatarov 9d5f87d86f Adds github test action 2025-02-20 10:43:23 +02:00
Yasen Pramatarov f0b487ca36 Reorganizes tests folder structure 2025-02-20 10:41:14 +02:00
Yasen Pramatarov 5327bde032 Adds tests for middleware 2025-02-19 15:31:01 +02:00
Yasen Pramatarov c2f63f6121 Adds security headers and CSRF protection tests 2025-02-19 11:08:42 +02:00
Yasen Pramatarov 9d0056f0a6 Adds transaction database methods (for the tests) 2025-02-18 16:46:56 +02:00
Yasen Pramatarov a399103305 Adds database execute and prepare (needed for the tests) 2025-02-18 16:45:25 +02:00
Yasen Pramatarov b7f8fce86e Replaces errors with exceptions in database class 2025-02-18 16:42:36 +02:00
Yasen Pramatarov c77b07b8a2 Removes the router test for now 2025-02-18 16:42:17 +02:00
Yasen Pramatarov 6fc3629014 Adds initial unit tests 2025-02-18 16:36:31 +02:00
Yasen Pramatarov 2da13af04c Bugfixes 2025-02-17 18:51:39 +02:00
Yasen Pramatarov 363fbf2a6b Reorganizes helper include files 2025-02-17 16:50:57 +02:00
Yasen Pramatarov 3953546ace Adds option to allow media on selected pages 2025-02-17 16:03:59 +02:00
Yasen Pramatarov b7e10363d0 Adds security headers include middleware 2025-02-17 15:52:46 +02:00
Yasen Pramatarov f53a3eef05 Fixes remnants of old messaging system 2025-02-17 15:47:36 +02:00
Yasen Pramatarov ae8d84012b Bugfixes 2025-02-17 15:41:35 +02:00
Yasen Pramatarov ddb86eae51 Bugfixes 2025-02-17 15:40:34 +02:00
Yasen Pramatarov 144dd6e742 Adds ratelimiting to some pages 2025-02-17 15:15:05 +02:00
Yasen Pramatarov c465fbfdf4 Pages ratelimit middleware 2025-02-17 15:05:44 +02:00
Yasen Pramatarov beafdf29fb Enhances ratelimiter to include page requests, configurable 2025-02-17 15:04:50 +02:00
Yasen Pramatarov 00e2a38087 Renames ratelimitTable to authRatelimitTable 2025-02-17 14:52:24 +02:00
Yasen Pramatarov 80bf3ee2ed Switches from session messages to feedback class ones 2025-02-17 14:46:19 +02:00
Yasen Pramatarov c32bbd518b Adds valifdation to profile page 2025-02-17 14:44:47 +02:00
Yasen Pramatarov 730a5c153e Adds session management 2025-02-17 14:36:00 +02:00
Yasen Pramatarov 3a9916e63b Renames messages to feedback 2025-02-17 10:24:50 +02:00
Yasen Pramatarov 3e9eb0d822 Renames messages to feedback 2025-02-16 10:18:26 +02:00
Yasen Pramatarov ef97dda39b Renames messages to feedback 2025-02-15 10:13:39 +02:00
Yasen Pramatarov 31f4a99d20 Removes hardcoded messages 2025-02-12 17:48:27 +02:00
Yasen Pramatarov 759059baad Adds new messages 2025-02-11 17:25:55 +02:00
Yasen Pramatarov cca0eb63a6 Adds validation to security pages 2025-02-10 19:33:24 +02:00
Yasen Pramatarov 6c37a082bf Adds validation to registration form 2025-02-10 19:25:17 +02:00
Yasen Pramatarov d2a9280d7d Omit sidebar for non-logged in users 2025-02-10 19:18:15 +02:00
Yasen Pramatarov 64d19f61f2 Bugfix, allows both "true" an "1" for registration enabled, 2025-02-06 13:18:19 +02:00
Yasen Pramatarov cadc7b7750 Adds validator class, to be used in forms validation 2025-02-06 13:14:29 +02:00
Yasen Pramatarov d84c015787 Fixes html 2025-01-30 18:55:09 +02:00
Yasen Pramatarov 27a4dca7c6 Adds CSRF checks to login/logout pages 2025-01-30 18:48:46 +02:00
Yasen Pramatarov 9c9a306f55 Implements security helper and CSRF middleware 2025-01-30 18:47:13 +02:00
Yasen Pramatarov be77376d85 Removes old bootstrap files 2025-01-30 13:47:29 +02:00
Yasen Pramatarov eecd74cc0f Foxes date and time on dashboard 2025-01-30 13:31:39 +02:00
Yasen Pramatarov b4df4b785a Fixes date and time display 2025-01-29 17:46:47 +02:00
Yasen Pramatarov 9b00e3d42c Rebuilds conferences page 2025-01-29 17:20:32 +02:00
Yasen Pramatarov 170e885251 Small final fixes to components and participants pages 2025-01-29 16:05:56 +02:00
Yasen Pramatarov a96b203021 HTML fixes 2025-01-29 15:56:08 +02:00
Yasen Pramatarov 057cc6dca5 Cleans up components page code 2025-01-29 15:55:45 +02:00
Yasen Pramatarov 11d4118e71 Rebuilds participants page 2025-01-29 15:47:10 +02:00
Yasen Pramatarov f13cad57d8 HTML fixes 2025-01-29 15:46:42 +02:00
Yasen Pramatarov b552a80203 Cleans up the old code 2025-01-29 10:46:18 +02:00
Yasen Pramatarov b971a76662 Rebuilds livejs pages 2025-01-29 10:46:06 +02:00
Yasen Pramatarov 25da7331f0 Adds more pages descriptions 2025-01-29 10:45:24 +02:00
Yasen Pramatarov 50b89f92ea Fixes HTML 2025-01-29 10:44:54 +02:00
Yasen Pramatarov 676e145349 Adds pages descrition 2025-01-29 10:43:37 +02:00
Yasen Pramatarov f952257c20 Cleans up old files 2025-01-28 21:19:47 +02:00
Yasen Pramatarov e6e91b19d0 Moves live configjs to separate page 2025-01-28 21:19:24 +02:00
Yasen Pramatarov 26c7660bfa Moves graphs to separate page 2025-01-28 21:18:48 +02:00
Yasen Pramatarov e50ac96b50 Moves latest data to separate page 2025-01-28 21:18:20 +02:00
Yasen Pramatarov 20a39f5c29 Reorganizes static files and libs 2025-01-28 21:16:34 +02:00
Yasen Pramatarov 6e4657e90f Fixes the html of agents pages 2025-01-28 15:54:42 +02:00
Yasen Pramatarov 779d3e0bf6 Updates latest data page functionality 2025-01-28 15:27:40 +02:00
Yasen Pramatarov a288d311c0 Troubleshoots platforms adding and deleting 2025-01-27 17:27:46 +02:00
Yasen Pramatarov f87c42a746 HTML fixes 2025-01-26 19:07:07 +02:00
Yasen Pramatarov 299327cf29 Fix config page title 2025-01-26 18:32:29 +02:00
Yasen Pramatarov eb512c4c1b Adds js messages to settings page 2025-01-26 18:18:43 +02:00
Yasen Pramatarov 7dfd50e19a Updates config page with the new js messages 2025-01-26 18:06:00 +02:00
Yasen Pramatarov dfdb24a550 Adds JS messages system in separate files for easy inclusion 2025-01-26 18:05:29 +02:00
Yasen Pramatarov e13bb7fc42 Moves our JS files in separate folder 2025-01-26 18:03:44 +02:00
Yasen Pramatarov 828020d689 Fixes config editing JS messages 2025-01-26 17:32:37 +02:00
Yasen Pramatarov 4a8185839d Fixes HTML of config page 2025-01-26 15:33:45 +02:00
Yasen Pramatarov 71d0984e9d Redesigns the components page 2025-01-26 14:39:10 +02:00
Yasen Pramatarov 4e79b76377 HTML fix 2025-01-26 14:07:58 +02:00
Yasen Pramatarov fc16bea465 HTML fixes 2025-01-26 14:07:28 +02:00
Yasen Pramatarov df200aae64 Redesignes and fixes the agents page 2025-01-26 00:11:19 +02:00
Yasen Pramatarov 06cc20fb2a Fixes bug in configjs and interfaceconfigjs pages 2025-01-24 16:13:45 +02:00
Yasen Pramatarov 5a451115f4 Highlights selected host or agent 2025-01-24 16:03:26 +02:00
Yasen Pramatarov fc71cdd7f8 Fixes status page design 2025-01-24 11:48:37 +02:00
Yasen Pramatarov e59920cfd0 Troubleshoots settings page 2025-01-23 18:40:55 +02:00
Yasen Pramatarov 6e6f4f6694 Desing fixes 2025-01-23 14:16:00 +02:00
Yasen Pramatarov 752f519ccc Enhances messages system with JS-based messages 2025-01-23 14:06:36 +02:00
Yasen Pramatarov ffe08f913b Updates design of config page, adds ajax 2025-01-23 12:42:27 +02:00
Yasen Pramatarov 1f75f81297 Moves Jitsi platforms configs to settings 2025-01-23 12:41:29 +02:00
Yasen Pramatarov b9e85c65bd Updates user rights 2025-01-23 12:40:11 +02:00
Yasen Pramatarov e3b8cccba3 Ad javascript option to messaging 2025-01-23 12:37:10 +02:00
Yasen Pramatarov 5f9702848e Redesigns the help page 2025-01-23 09:53:08 +02:00
Yasen Pramatarov 5dc419b7a7 Fixes deletion of platforms with hosts/agents in them 2025-01-23 00:26:40 +02:00
Yasen Pramatarov aa2dcc027d Makes required fields for hosts and agents 2025-01-23 00:17:09 +02:00
Yasen Pramatarov f0b98d3063 Simplifies template for logs page 2025-01-23 00:15:52 +02:00
Yasen Pramatarov 5b24d098e4 Debugs config items management 2025-01-22 22:52:50 +02:00
Yasen Pramatarov 53b3965a32 Improves design of config items delete 2025-01-22 18:45:23 +02:00
Yasen Pramatarov d0fa120202 Fixes SQL for platform->host->agent 2025-01-22 17:49:15 +02:00
Yasen Pramatarov fc1ed97499 Fixes platform->host->agent logic 2025-01-22 17:46:08 +02:00
Yasen Pramatarov e3f839bc56 Puts all jilo config in one page with ajax 2025-01-21 16:57:28 +02:00
Yasen Pramatarov d45ba62805 Moves agent editing to in-place ajax forms 2025-01-20 21:42:22 +02:00
Yasen Pramatarov 5321942da8 Moved platform and host editing to in-place ajax 2025-01-20 21:19:33 +02:00
Yasen Pramatarov 405f58124d Redesigns the whole config page 2025-01-20 21:17:54 +02:00
Yasen Pramatarov 1e4ebae652 Fixes cancel button 2025-01-20 21:17:28 +02:00
Yasen Pramatarov e932e4899c Alignes page title 2025-01-20 21:16:26 +02:00
Yasen Pramatarov 7c8335d3e7 Removes unused old config templates 2025-01-20 21:15:29 +02:00
Yasen Pramatarov 81287a2c95 Removes port from host config 2025-01-20 21:14:43 +02:00
Yasen Pramatarov 9c3964da20 Adds agents management pages 2025-01-19 17:46:50 +02:00
Yasen Pramatarov 3c9cce2c8b Fixes sidebar menu entries 2025-01-18 19:41:28 +02:00
Yasen Pramatarov 35020a0108 Fixes logs search 2025-01-18 17:41:03 +02:00
Yasen Pramatarov e85292b58f Adds proper pagination. Paginates logs page. 2025-01-18 17:24:30 +02:00
Yasen Pramatarov 9fd2af6538 Updates design and fixes the logs page 2025-01-18 13:17:32 +02:00
Yasen Pramatarov 949ce27f63 HTML fixes 2025-01-17 18:29:41 +02:00
Yasen Pramatarov 81b66db3c6 Updates changelog 2025-01-17 17:59:58 +02:00
Yasen Pramatarov b2fcaf6793 HTML fixes 2025-01-17 16:08:37 +02:00
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
Yasen Pramatarov e76c78fde4 Prepares for release 0.2.1 2024-10-17 12:03:13 +03:00
Yasen Pramatarov 15d5626b20 Adds Jilo agents checks table to the schema 2024-10-15 20:52:16 +03:00
Yasen Pramatarov 202d90d4fc Adds a check period field in agents config 2024-10-15 20:48:59 +03:00
Yasen Pramatarov 7e09815835 Adds example data notice in charts under construction 2024-10-13 11:32:57 +03:00
Yasen Pramatarov 0318670bce Fixes custom range on mouse zoom 2024-10-12 17:02:07 +03:00
Yasen Pramatarov 6b4f344bfd Adds initial "latest data" code 2024-10-11 10:58:08 +03:00
Yasen Pramatarov 1d706803b1 Adds active state to last clicked graph button 2024-10-11 10:22:20 +03:00
Yasen Pramatarov 88a3907d29 Adds Chart.js license 2024-10-11 09:52:40 +03:00
Yasen Pramatarov 71c636f297 Styles the time range buttons 2024-10-10 12:42:32 +03:00
Yasen Pramatarov c04fcb7d42 Fixes graphs and adds mouse selection 2024-10-10 11:31:05 +03:00
Yasen Pramatarov 4b1ab93474 Uses a common graphs helper template 2024-10-09 15:56:51 +03:00
Yasen Pramatarov b81be03c4f Adds graphs page with some temporary example data 2024-10-09 14:53:57 +03:00
Yasen Pramatarov 1756ee71cd Adds graphs and latest data pages 2024-10-08 13:23:38 +03:00
Yasen Pramatarov 537486d591 Design fixes 2024-10-07 10:42:54 +03:00
Yasen Pramatarov 865d21e2aa Adds a help page 2024-10-07 10:00:15 +03:00
Yasen Pramatarov ef698c4fa7 Makes active platform button a span instead of a link 2024-10-06 18:22:05 +03:00
Yasen Pramatarov 9fcaa76644 Fixes the design and left menu 2024-10-05 18:12:15 +03:00
Yasen Pramatarov 0b9e1118b8 updates the changelog 2024-10-04 18:42:32 +03:00
Yasen Pramatarov d82219eea0 bugfixes 2024-10-04 18:13:55 +03:00
Yasen Pramatarov 37c5bdb4b4 Sanitize all output 2024-10-04 14:18:28 +03:00
Yasen Pramatarov f4a64b6887 Centralizes data sanitation 2024-10-04 11:36:45 +03:00
Yasen Pramatarov b3f642c02b Links to participants in conference view 2024-10-03 19:48:29 +03:00
Yasen Pramatarov fbe646823d Fixes front page conferences counting 2024-10-03 19:33:31 +03:00
Yasen Pramatarov 9c187c9550 Adds ellipses to pagination 2024-10-03 18:47:23 +03:00
Yasen Pramatarov eb8104595e Fixes bugs in components view 2024-10-03 17:31:39 +03:00
Yasen Pramatarov 2d8bc53195 Compacts the agent config listing 2024-10-03 11:40:41 +03:00
Yasen Pramatarov 1468843cac Fixes agent types selection on add/edit 2024-10-03 11:28:23 +03:00
Yasen Pramatarov d0ef53a176 Fixes AJAX agent cache buttons 2024-10-03 10:59:32 +03:00
Yasen Pramatarov 0791a4c2c9 CSS fixes 2024-10-03 09:43:37 +03:00
Yasen Pramatarov 4f0601cff7 Fixes timestamps in cache 2024-10-02 22:50:27 +03:00
Yasen Pramatarov 4e19a1c571 Enhances agent data display 2024-10-02 22:33:09 +03:00
Yasen Pramatarov 784532c44d Fixes cache buttons for agents data 2024-10-02 16:35:10 +03:00
Yasen Pramatarov 4e827e62f0 Fixes the calls to agent with JWT 2024-10-02 14:04:45 +03:00
Yasen Pramatarov 8bb2e8c838 Prepares for agents' JWT 2024-10-01 10:18:53 +03:00
Yasen Pramatarov ff2bc61bda Fixes bottons tooltips 2024-09-30 21:56:53 +03:00
Yasen Pramatarov b6c42fd4ec Fixes agent buttons 2024-09-30 19:48:00 +03:00
Yasen Pramatarov 37b0a289b2 Fixes logout 2024-09-30 12:52:06 +03:00
Yasen Pramatarov e110619835 Small bugfixes 2024-09-30 12:22:53 +03:00
Yasen Pramatarov b72d4ea791 Fixes to the agent API calls 2024-09-30 11:55:23 +03:00
Yasen Pramatarov 37398b5986 Fixes agent calls and adds error reporting 2024-09-30 11:09:01 +03:00
Yasen Pramatarov e1888cce8a Typo fix 2024-09-30 10:07:50 +03:00
Yasen Pramatarov a585bdaaff Initial test with agents API calling 2024-09-29 10:07:04 +03:00
Yasen Pramatarov 24c5870d33 Adds jilo agent API call func 2024-09-28 10:04:20 +03:00
Yasen Pramatarov ed2a058c12 Checks agent cache 2024-09-26 09:56:24 +03:00
Yasen Pramatarov ede9ecc7b6 Adds initial ajax call to agents api 2024-09-27 09:49:50 +03:00
Yasen Pramatarov e5d1324126 Fixes platform switcher 2024-09-25 09:51:26 +03:00
Yasen Pramatarov 6cc1efff15 Adds platforms switcher in menu 2024-09-24 09:39:30 +03:00
Yasen Pramatarov ff2a96119e Adds initial agents checks page 2024-09-23 21:39:32 +03:00
Yasen Pramatarov eabbd67b9a Hides time when the sidebar is collapsed 2024-09-23 19:37:58 +03:00
Yasen Pramatarov 950ea8ff95 Adds internal anchor links in the config page 2024-09-23 19:33:10 +03:00
Yasen Pramatarov 1acb7126ec Moving all of agents config to "config" page 2024-09-23 12:39:33 +03:00
Yasen Pramatarov 6d07744ab7 Fixes agent config 2024-09-22 12:26:19 +03:00
Yasen Pramatarov 72b1442b77 Fixes agent config pages 2024-09-21 12:21:46 +03:00
Yasen Pramatarov 88e77f71ef Fixes config pages 2024-09-20 12:13:18 +03:00
Yasen Pramatarov 847dc280d3 Fixes agents config 2024-09-19 11:41:59 +03:00
Yasen Pramatarov 4786b0d1f7 Fixes the agents configuration. 2024-09-18 11:32:24 +03:00
Yasen Pramatarov be0cd48c01 Adds user IP to the logs. 2024-09-17 14:22:43 +03:00
Yasen Pramatarov 527da25cdc Tidying up CSS 2024-09-17 13:07:10 +03:00
Yasen Pramatarov 33dfbcdeea Fixes to the logging system 2024-09-16 19:08:03 +03:00
Yasen Pramatarov 1c710bef35 Updates changelog 2024-09-16 17:10:14 +03:00
Yasen Pramatarov f6362bfdc1 Adds initial support for logs 2024-09-16 17:09:37 +03:00
Yasen Pramatarov 5d06a7222c Fixes the pagination for components 2024-09-15 21:49:20 +03:00
Yasen Pramatarov 76e061e1cb updates changelog 2024-09-15 21:43:44 +03:00
Yasen Pramatarov 2478f84e85 Adds pagination 2024-09-15 21:42:47 +03:00
Yasen Pramatarov 8a2f082b09 Adds missing file 2024-09-13 14:07:36 +03:00
Yasen Pramatarov 42b42e6fd2 Renames front page to "dashboard 2024-09-13 14:04:14 +03:00
Yasen Pramatarov 3170d87934 Adds "not found" page 2024-09-13 14:02:59 +03:00
Yasen Pramatarov 6ec0981b0a Fixes bugs and cleans up the code 2024-09-13 13:49:17 +03:00
Yasen Pramatarov e0eee38726 Bugfixes to the latest changes 2024-09-13 13:04:15 +03:00
Yasen Pramatarov 7cc8da562d Adds access control to pages based on user rights 2024-09-13 12:13:00 +03:00
Yasen Pramatarov 172a545acf Reorganizes the pages loading in index.php 2024-09-13 11:05:11 +03:00
Yasen Pramatarov 318f5356c0 Fixes error in sql schema 2024-09-12 12:34:22 +03:00
Yasen Pramatarov bed55c909d User 1 is always superuser, User 2 is always demo 2024-09-12 12:28:02 +03:00
Yasen Pramatarov 8da45a06d0 Makes minor fixes to the rights management 2024-09-12 11:54:57 +03:00
Yasen Pramatarov d2154fa63c Adds user rights editing feature 2024-09-11 22:51:46 +03:00
Yasen Pramatarov e195b653b1 Adds favicon 2024-09-11 19:34:48 +03:00
Yasen Pramatarov 70163e1c5e Adds initial support for user rights 2024-09-11 16:21:05 +03:00
Yasen Pramatarov 1efb3b6a17 Adds autofocus to forms 2024-09-10 18:29:58 +03:00
Yasen Pramatarov 59c6706505 updates the changelog 2024-09-10 14:07:49 +03:00
Yasen Pramatarov 6a48b54320 Streamlines the avatar management 2024-09-10 14:05:38 +03:00
Yasen Pramatarov da5e5ec4ae Fixes avatar management 2024-09-09 16:35:06 +03:00
Yasen Pramatarov 573df0fe4f Fixes avatar management 2024-09-09 15:54:32 +03:00
Yasen Pramatarov fd949f6f4f Adds user avatar management 2024-09-09 15:20:21 +03:00
Yasen Pramatarov 84664f8b5f Fixes default avatar 2024-09-08 14:17:08 +03:00
Yasen Pramatarov d2f4850d28 Adds default avatar image 2024-09-08 14:15:08 +03:00
Yasen Pramatarov f0f7d5b2d3 Adds basic profile editing 2024-09-08 13:48:21 +03:00
Yasen Pramatarov 06a534c2da Adds profile edit page 2024-09-08 01:36:57 +03:00
Yasen Pramatarov 189d30bad5 Enhances the user profile 2024-09-07 23:05:22 +03:00
Yasen Pramatarov 993474a754 Adds users meta and rights 2024-09-07 21:49:50 +03:00
Yasen Pramatarov d64fc5cf56 Cleans up style 2024-09-06 19:34:03 +03:00
Yasen Pramatarov 2d4205916b Fixes bug in config editing 2024-09-06 19:03:08 +03:00
Yasen Pramatarov de7133be3d Adds support for managing Jilo Agents 2024-09-05 01:06:38 +03:00
Yasen Pramatarov 48a0cf9e86 Fixes error in web path 2024-09-04 13:11:17 +03:00
Yasen Pramatarov dc9462260d Fixes platform config rendering 2024-09-04 12:53:45 +03:00
Yasen Pramatarov b60208bea7 Moves the platforms config from flat file to SQLite DB 2024-09-04 12:53:02 +03:00
Yasen Pramatarov 7d85c9181d Prepares for releasing v0.2 2024-08-31 20:24:12 +03:00
Yasen Pramatarov f88fc0f819 Adds switching between raw config files and only active config lines 2024-08-31 19:50:58 +03:00
Yasen Pramatarov 54d6ce2ec4 Updates CHANGELOG - config.js & interface_config.js 2024-08-30 13:03:13 +03:00
Yasen Pramatarov 0bab284e99 Adds interface_config.js capability 2024-08-29 13:01:46 +03:00
Yasen Pramatarov cf7d417193 Adds config.js capability 2024-08-28 12:59:13 +03:00
Yasen Pramatarov 1780233778 Adds Jitsi URL for loading the config.js and doing checks 2024-08-27 21:18:27 +03:00
Yasen Pramatarov 666f3ca98b Fixes adding, editing and deleting a platform 2024-08-26 21:19:21 +03:00
Yasen Pramatarov 1ba32d86f7 Move the platform editing to config page 2024-08-25 13:24:48 +03:00
Yasen Pramatarov d2213ca3e9 Adds "platform_id" to all links 2024-08-24 13:06:00 +03:00
Yasen Pramatarov c83baeee2f Moves all SQL into the respective classes 2024-08-23 12:44:31 +03:00
Yasen Pramatarov cd7b78a2e5 Fixe SQL 2024-08-22 11:59:48 +03:00
Yasen Pramatarov d854473a06 Moving SQL into classes 2024-08-21 22:11:28 +03:00
Yasen Pramatarov bf197ae96b Fixes regexps 2024-08-20 12:58:55 +03:00
Yasen Pramatarov 2bbc4af068 Fixes error handling 2024-08-19 13:42:16 +03:00
Yasen Pramatarov 1cda74cd50 Adds platform editing code 2024-08-19 13:25:09 +03:00
Yasen Pramatarov 3abafe2de7 Platforms edit code 2024-08-18 22:12:45 +03:00
Yasen Pramatarov 3a5eed933e Iniial edit platform code 2024-08-18 22:06:24 +03:00
Yasen Pramatarov 909fbc2626 Better error reporting 2024-08-17 11:20:08 +03:00
Yasen Pramatarov 09667920a2 Updates CHANGELOG 2024-08-17 10:02:55 +03:00
Yasen Pramatarov ea4b85ddf7 Initial support for multiple Jitsi platforms 2024-08-17 10:01:48 +03:00
Yasen Pramatarov 8ca9643fc2 Fixes install script 2024-08-16 09:35:21 +03:00
Yasen Pramatarov b02d72b29d Adds app root in links 2024-08-15 10:31:26 +03:00
Yasen Pramatarov e84c880289 Apache fixes 2024-08-14 10:11:36 +03:00
Yasen Pramatarov 1df19d9609 Updates CHANGELOG 2024-08-13 23:01:31 +03:00
Yasen Pramatarov a6cf2fe99b Updates config file 2024-08-13 22:59:11 +03:00
Yasen Pramatarov f796e100f8 Fixes config rendering of nested config arrays 2024-08-13 22:46:56 +03:00
Yasen Pramatarov 8eed6afd46 Fixes directory structure 2024-08-13 17:53:52 +03:00
Yasen Pramatarov 2cdfbe6e86 Updates config file 2024-08-13 14:24:56 +03:00
Yasen Pramatarov 9d3a4557c1 Updates README to match new folder structure 2024-08-13 14:16:15 +03:00
Yasen Pramatarov acd5ff0a23 Separates app code from public files. 2024-08-12 14:12:24 +03:00
Yasen Pramatarov 9a3024cfa6 Fixes error in database selection in helper function 2024-08-11 13:23:06 +03:00
Yasen Pramatarov 20d9dc0e73 Removes old unused cookie code 2024-08-11 13:15:18 +03:00
Yasen Pramatarov 8e123cf5f2 Adds time range helper function 2024-08-11 13:13:59 +03:00
Yasen Pramatarov 8765fe22c0 Updates CHANGELOG 2024-08-10 21:59:15 +03:00
Yasen Pramatarov e6d6a10795 Fixes error in database selection 2024-08-10 21:57:41 +03:00
Yasen Pramatarov aeb837fee5 Adds database helper functions 2024-08-10 21:42:44 +03:00
Yasen Pramatarov 91aac20998 Fixes error in JICOFO conferences stats 2024-08-09 12:17:05 +03:00
Yasen Pramatarov c915bcba9e Monthly widget fixes 2024-08-09 11:53:58 +03:00
Yasen Pramatarov 0f868f07e2 monthly stats widget fixes 2024-08-08 11:49:10 +03:00
Yasen Pramatarov 3943ba49b6 Fixes sidebar collapsing 2024-08-07 17:41:56 +03:00
Yasen Pramatarov 8a4816a6ae Updates CHANGELOG 2024-08-07 12:08:29 +03:00
Yasen Pramatarov 4b783a6b63 Adds sidebar icons 2024-08-07 12:03:54 +03:00
Yasen Pramatarov 6191d44944 HTML fixes 2024-08-07 11:16:54 +03:00
Yasen Pramatarov 838b8ea9f2 HTML fixes 2024-08-07 11:13:55 +03:00
Yasen Pramatarov 15c97859a4 Cleans up login and register pages. 2024-08-06 10:40:52 +03:00
Yasen Pramatarov 6199dad2c6 Design fixes 2024-08-06 09:56:27 +03:00
Yasen Pramatarov cb040ee408 Fixes logo 2024-08-06 09:32:14 +03:00
Yasen Pramatarov 2aaf2ac03f Adds logo 2024-08-05 22:25:32 +03:00
Yasen Pramatarov 27304513d8 Fixes sliding sidebar 2024-08-05 22:24:35 +03:00
Yasen Pramatarov 0cfa9d811e Fixes sidebar sliding 2024-08-05 20:21:34 +03:00
Yasen Pramatarov 54b998a1a8 Fixes sidebar sliding 2024-08-05 19:53:24 +03:00
Yasen Pramatarov 77e674bab5 Fixes sidebar 2024-08-05 17:08:13 +03:00
Yasen Pramatarov b6fc4a995d Fixes sidebar sliding 2024-08-05 17:02:35 +03:00
Yasen Pramatarov ca95f3c27e Fixes sidebar sliding 2024-08-04 16:33:41 +03:00
Yasen Pramatarov 22e502c90c Fixes sidebar 2024-08-03 16:25:42 +03:00
Yasen Pramatarov ae7929b4e1 Initial version of sidebar 2024-08-02 16:07:50 +03:00
Yasen Pramatarov 80454de3f2 UpdateS CHANGELOG 2024-08-01 11:24:44 +03:00
Yasen Pramatarov 8455a98ec5 Searches for default config file 2024-08-01 11:20:31 +03:00
Yasen Pramatarov 5d240b1dd8 Adds login and registration messages and configs 2024-08-01 11:12:54 +03:00
Yasen Pramatarov 06ffde67f4 Fixes SQL 2024-08-01 10:52:39 +03:00
Yasen Pramatarov 8bbe4b6274 Fixes SQL 2024-08-01 10:18:33 +03:00
Yasen Pramatarov 001e6c5be7 Updates CHANGELOG 2024-07-31 21:55:26 +03:00
Yasen Pramatarov 55b53ef30a Adds participants to monthly stats widget 2024-07-31 21:54:30 +03:00
Yasen Pramatarov c011321403 Fixes SQL 2024-07-31 13:18:45 +03:00
Yasen Pramatarov 42f9738128 Adds widget for monthly conferences number 2024-07-30 12:54:29 +03:00
Yasen Pramatarov c0c072884f Cleans up html code from models and controllers. 2024-07-29 16:50:16 +03:00
Yasen Pramatarov 54ddc6577e Adds widgets to participants page 2024-07-29 16:38:44 +03:00
Yasen Pramatarov d9aaf97b81 HTML fixes 2024-07-29 16:10:35 +03:00
Yasen Pramatarov f76fd03794 Adds widgets to conferences page 2024-07-29 15:56:08 +03:00
Yasen Pramatarov 3e0729c6cc Adds widget to components page 2024-07-28 13:35:09 +03:00
Yasen Pramatarov 16b7627b60 Fixes front page widgets 2024-07-27 12:36:49 +03:00
Yasen Pramatarov c6e52df33a Updates CHANGELOG 2024-07-27 12:30:16 +03:00
Yasen Pramatarov 7d459cf508 Implements front page widgets 2024-07-27 12:29:28 +03:00
Yasen Pramatarov 350ba053e8 Initial version of widgets 2024-07-26 11:55:58 +03:00
Yasen Pramatarov efd7cd74a2 Updates README 2024-07-25 20:03:11 +03:00
Yasen Pramatarov c39e53906f Updates README 2024-07-25 19:58:14 +03:00
Yasen Pramatarov 5463ea0cee Updates licenses information. 2024-07-25 19:51:05 +03:00
Yasen Pramatarov ba9d14f5d1 Added jquery license 2024-07-25 19:49:10 +03:00
Yasen Pramatarov 793600ccb9 Prepares for release 0.1.1 2024-07-25 19:10:53 +03:00
Yasen Pramatarov ac42b6f591 Cleans up bootstrap CSS and front page collapsing wodgets 2024-07-25 16:28:51 +03:00
Yasen Pramatarov fa581f8fb7 Cleans up HTML 2024-07-25 15:13:12 +03:00
Yasen Pramatarov 54678bc329 Cleaning up HTML 2024-07-25 12:24:07 +03:00
Yasen Pramatarov 6400854316 Cleaning up HTML 2024-07-25 12:15:53 +03:00
Yasen Pramatarov 462e7a45b8 Cleaning up HTML 2024-07-25 10:43:23 +03:00
Yasen Pramatarov fa61eecb15 Adds Bootstrap 2024-07-24 17:08:13 +03:00
Yasen Pramatarov 2eb18f0e84 Install script fixes 2024-07-24 16:50:48 +03:00
Yasen Pramatarov d8a756e68a Install script fixes 2024-07-24 16:46:20 +03:00
Yasen Pramatarov e32815dde0 Version var fix 2024-07-23 15:36:24 +03:00
Yasen Pramatarov 825b84ddc8 Install script fixes 2024-07-22 15:33:42 +03:00
Yasen Pramatarov 13a2f2482a Updates CHANGELOG 2024-07-21 11:29:00 +03:00
Yasen Pramatarov acb314f9fe Install script fixes 2024-07-20 11:27:15 +03:00
Yasen Pramatarov 01a497a780 Install script fixes 2024-07-19 11:24:38 +03:00
Yasen Pramatarov cd83494654 Install script fixes 2024-07-19 11:20:08 +03:00
Yasen Pramatarov 6596e97889 Install script fixes 2024-07-19 11:14:30 +03:00
Yasen Pramatarov 7e5e68dc23 Install script fixes 2024-07-18 20:00:05 +03:00
Yasen Pramatarov 08f69dcf05 Install script fixes 2024-07-17 19:51:49 +03:00
Yasen Pramatarov b9d7ac69f5 packaging fixes 2024-07-16 19:40:24 +03:00
Yasen Pramatarov 05672680e4 packaginf fixes 2024-07-15 19:37:53 +03:00
Yasen Pramatarov d5a628a200 RPM packaging fixes 2024-07-14 15:00:53 +03:00
Yasen Pramatarov 714800b5c0 Adds initial RPM build script 2024-07-13 14:49:31 +03:00
Yasen Pramatarov fefba65e25 RPM initial files 2024-07-12 08:19:41 +03:00
Yasen Pramatarov dc5d074690 DEB packaging fixes 2024-07-11 14:24:45 +03:00
Yasen Pramatarov 91fdba99b2 Updates README 2024-07-10 19:07:31 +03:00
Yasen Pramatarov 4a9ee48705 Updates README 2024-07-10 19:04:50 +03:00
Yasen Pramatarov a4e644261a Update README.md 2024-07-10 13:34:26 +00:00
Yasen Pramatarov 38e15c8cf6 Adds conference duration to front page widgets 2024-07-10 15:55:16 +03:00
Yasen Pramatarov 2496d3fc93 Adds conference duration calculation 2024-07-10 15:52:13 +03:00
Yasen Pramatarov e85e9c577f Packaging fixes 2024-07-10 13:58:44 +03:00
Yasen Pramatarov 3b3a5c205a Initial deb packaging 2024-07-09 13:51:03 +03:00
Yasen Pramatarov 18e215c92f Updates CHANGELOG 2024-07-08 20:37:15 +03:00
270 changed files with 31165 additions and 2249 deletions

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

@ -0,0 +1,121 @@
name: PHP Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
runs-on: ubuntu-latest
env:
DB_TYPE: mariadb
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: jilo_test
DB_USERNAME: test_jilo
DB_PASSWORD: test_password
services:
mariadb:
image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: jilo_test
MARIADB_USER: test_jilo
MARIADB_PASSWORD: test_password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h127.0.0.1 -P3306 -uroot -proot"
--health-interval=10s
--health-timeout=10s
--health-retries=5
--health-start-period=30s
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: pdo, pdo_mysql, xdebug
coverage: xdebug
- name: Wait for MariaDB
run: |
sudo apt-get update
sudo apt-get install -y mariadb-client
while ! mysqladmin ping -h"127.0.0.1" -P"3306" -uroot -proot --silent; do
echo "Waiting for database connection..."
sleep 2
done
- name: Install dependencies
run: |
cd tests
composer install
- name: Test database connection
run: |
mysql -h127.0.0.1 -P3306 -uroot -proot -e "SHOW DATABASES;"
mysql -h127.0.0.1 -P3306 -uroot -proot -e "SELECT User,Host FROM mysql.user;"
mysql -h127.0.0.1 -P3306 -uroot -proot -e "GRANT ALL PRIVILEGES ON jilo_test.* TO 'test_jilo'@'%';"
mysql -h127.0.0.1 -P3306 -uroot -proot -e "FLUSH PRIVILEGES;"
- name: Update database config for CI
run: |
# Create temporary test config
mkdir -p tests/config
cat > tests/config/ci-config.php << 'EOF'
<?php
define('CI_DB_PASSWORD', 'test_password');
define('CI_DB_HOST', '127.0.0.1');
EOF
# Verify config file was created
echo "Config file contents:"
cat tests/config/ci-config.php
echo "\nConfig file location:"
ls -la tests/config/ci-config.php
# Grant access from Docker network
mysql -h127.0.0.1 -P3306 -uroot -proot -e "
DROP USER IF EXISTS 'test_jilo'@'%';
CREATE USER 'test_jilo'@'%' IDENTIFIED BY 'test_password';
GRANT ALL PRIVILEGES ON jilo_test.* TO 'test_jilo'@'%';
CREATE DATABASE IF NOT EXISTS jilo_test;
FLUSH PRIVILEGES;
"
# Update test files to require the config (using absolute path)
CONFIG_PATH=$(realpath tests/config/ci-config.php)
echo "\nConfig path: $CONFIG_PATH"
# Add require statement at the very start
for file in tests/Unit/Classes/{DatabaseTest,UserTest}.php; do
echo "<?php" > "$file.tmp"
echo "require_once '$CONFIG_PATH';" >> "$file.tmp"
tail -n +2 "$file" >> "$file.tmp"
mv "$file.tmp" "$file"
echo "\nFirst 5 lines of $file:"
head -n 5 "$file"
done
# Test database connection directly
echo "\nTesting database connection:"
mysql -h127.0.0.1 -P3306 -utest_jilo -ptest_password -e "SELECT 'Database connection successful!'" || echo "Database connection failed"
- name: Run test suite
run: |
cd tests
./vendor/bin/phpunit
# FIXME
# XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html coverage
# env:
# COMPOSER_PROCESS_TIMEOUT: 0
# COMPOSER_NO_INTERACTION: 1
# COMPOSER_NO_AUDIT: 1

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ jilo.db
jilo-web.db
packaging/deb-package/
packaging/rpm-package/
/public_html/uploads/avatars/

View File

@ -7,10 +7,252 @@ All notable changes to this project will be documented in this file.
## Unreleased
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web
- codeberg: https://codeberg.org/lindeas/jilo-web
- github: https://github.com/lindeas/jilo-web
- gitlab: https://gitlab.com/lindeas/jilo-web
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.4.1...HEAD
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.4.1...HEAD
- github: https://github.com/lindeas/jilo-web/compare/v0.4.1...HEAD
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.4.1...HEAD
### Added
- CSS for dashboard widgets
- Monthly dashboard statistics redesign
- Tracking of applied database migrations in the database
- Option to run database migrations one by one
- Log Throttler to prevent log flooding
- Logger helper with fallback when no log plugin is available
- Plugin asset page
- Plugin hooks for profile page, account menu, and asset loading
- Email helper and email templates, including password reset email
- Admin page and admin dashboard for all administrative tasks
- Plugin namagement section for the admin dashboard
### Changed
- Updated credentials pages and removed unused "credentials.php"
- Redesigned admin tools, themes, profile, credentials/2FA, and authentication pages
- Redesigned sidebar, main elements, menus, and overall CSS
- Updated pagination styling
- Reorganized dashboard layout
- Switched profile edit and action pages to uniform action-card design
- Replaced "error_log" with "app_log" in 2FA
- Updated index bootstrap to use global "APP_PATH"
- Refactored database migration system and Admin Tools functionality
- Removed "admin-tools" page, all functionality is now in "admin" page
### Fixed
- Database migration reliability issues
- Validator rejecting "0" as a valid value
- Collapsing sidebar layout issues
- Profile avatar upload issues
- Public pages incorrectly requiring authentication
- Correct encoding of login redirect URL parameters
---
## 0.4.1 - 2025-11-13
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.4...v0.4.1
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.4...v0.4.1
- github: https://github.com/lindeas/jilo-web/compare/v0.4...v0.4.1
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.4...v0.4.1
### Added
- Added the ability to have non-sanitized feedback messages
- Added a notice for maintenance mode for superusers
- Added initial support for maintenance mode
- Added initial support for database upgrades and migrations
- Added CSRF protection to the theme switcher functionality
- Added a helper to manage all static assets of a theme
- Added theme data to be passed correctly to the views
- Added "modern" and "retro" theme screenshots
- Added "alternative retro" theme
- Added CSS and JS to the default theme
- Added change theme menu entry
### Changed
- Moved away from SQLite to MariaDB/MySQL for the main Jilo website
- Integrated Highlight.js library into the SQL view modal for better code highlighting
- Moved the migration flag to the database with a fallback to a file
- Made the theme setting configuration per-user instead of global
- Refactored the session class and added a random session name generator if not configured
- Moved session variables to the configuration file
### Fixed
- Fixed flash messages to show up only once per page
- Fixed database migration functionality and associated feedback notices
- Fixed theme folder structure, helpers, and display logic to work correctly with new asset management
- Fixed index routing to work with the latest session and config changes
- Fixed the router class and several bugs within the session class and theme switcher functionality
### Removed
- Removed getScreenshotUrl function, using the generic getAssetUrl for all assets instead
---
## 0.4 - 2025-04-12
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.3...v0.4
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.3...v0.4
- github: https://github.com/lindeas/jilo-web/compare/v0.3...v0.4
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.3...v0.4
### Added
- Added top-right menu with profile, admin, and docs sections
- Added two-factor authentication
- Added resetting of forgotten password
- Added login credentials management
- Added proper pagination
- Added agents managemet pages
- Added javascript-based feedback messages
- Added description to each page
- Added CSRF checks
- Added validator class for all forms
- Added rate limiting to all pages
- Added authentication rate limiting to login and registration
- Added unit tests
- Added integration/feature tests
- Added testing workflow for github
### Changed
- Increased session to 2 hours w/out "remember me", 30 days with
- Made the config editing in-place with AJAX
- Redesigned the help page
- Moved graphs and latest data to their own pages
- Moved live config.js to its own page
- Redesigned the messages system and renamed them to feedback messages
### Fixed
- Bugfixes
- Fixed config editing
- Fixed logs search
- Removed hardcoded messages, changed to feedback messages
---
## 0.3 - 2025-01-15
#### Links
- 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
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.2...v0.2.1
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.2...v0.2.1
- github: https://github.com/lindeas/jilo-web/compare/v0.2...v0.2.1
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.2...v0.2.1
### Added
- Added support for managing Jilo Agents
- Authenticating to Jilo Agents with JWT tokens with a shared secret key
- Added Jilo Agent functionality to fetch data, cache it and manage the cache
- Added more fields and avatar image to user profile
- Added pagination (with ellipses) for the longer listings
- Added initial support for application logs
- Added help page
- Added support for graphs by Chart.js
- Added "graphs" section in sidebar with graphs and latest data pages
### Changed
- Jitsi platforms config moved from file to SQLite database
- Left sidebar menu items reordered
### Fixed
- All output HTML sanitized
- Sanitized input forms data
- Fixed error in calculation of monthly total conferences on front page
---
## 0.2 - 2024-08-31
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.1.1...v0.2
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.1.1...v0.2
- github: https://github.com/lindeas/jilo-web/compare/v0.1.1...v0.2
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.1.1...v0.2
### Added
- Added collapsible front page widgets
- Added widgets to conferences, participants and components pages
- Added front page widget for monthly conferences and participants number
- Added login/registration control and messages
- Added default config file locations
- Added left collapsible sidebar
- Added logo
- Added database helper functions
- Added support for multiple Jitsi platforms
- Added app environments "production" and "development"
- Added visualisation of config.js and interface_config.js per Jitsi platform
### Changed
- MVC design - models(classes folder), views(templates folder) and controllers(pages folder)
- Changed menus - categories on sidebar menu, jitsi platforms on top menu
- Moved all the app code outside of the public web folder
- Config file now can have nested arrays
### Fixed
- Fixed SQL when conferences start and end time are not explicitly clear
- Web design fixes
- Fixed install script
---
## 0.1.1 - 2024-07-25
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.1...v0.1.1
- codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.1...v0.1.1
- github: https://github.com/lindeas/jilo-web/compare/v0.1...v0.1.1
- gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.1...v0.1.1
### Added
- Added duration calculation in conferences listing
- Added manual install script
- Added DEB and RPM build files
- Added Bootstrap (licensed under MIT)
### Changed
- Changed the layout with bootstrap CSS classes
---
## 0.1 - 2024-07-08
#### Links
- upstream: https://code.lindeas.com/lindeas/jilo-web/releases/tag/v0.1
- codeberg: https://codeberg.org/lindeas/jilo-web/releases/tag/v0.1
- github: https://github.com/lindeas/jilo-web/releases/tag/v0.1
- gitlab: https://gitlab.com/lindeas/jilo-web/-/releases/v0.1
### Added
- Initial version
@ -23,9 +265,3 @@ All notable changes to this project will be documented in this file.
- Added demo installation on https://work.lindeas.com/jilo-web-demo/
- Added participant search page
- Added component events search page
### Changed
### Fixed
---

View File

@ -1,25 +1,80 @@
# Jilo Web
Jilo Web is a PHP web interface to Jilo (JItsi Logs Observer).
## overview
Jilo Web is a PHP web interface to **[Jilo](https://work.lindeas.com/redirect.php?url=jilo)** (JItsi Logs Observer).
To have a working installation, in addition to the code here you need a jilo database file, generated by Jilo.
The webpage for this project is https://lindeas.com/jilo. There you will find information about both Jilo and Jilo Web.
The main git repo of **Jilo Web** is:
- https://code.lindeas.com/lindeas/jilo-web
It is mirrored at:
- https://codeberg.org/lindeas/jilo-web
- https://github.com/lindeas/jilo-web
- https://gitlab.com/lindeas/jilo-web
You can use any of these git repos to get the program.
You are welcome to send feedback with issues, comments and pull requests to a git mirror you prefer.
## demo
To see a demo install, go to https://work.lindeas.com/jilo-web-demo/
## version
Current version: **0.4.1** released on **2025-11-13**
## license
This project is licensed under the GNU General Public License version 2 (GPL-2.0). See LICENSE file.
Bootstrap is used in this project and is licensed under the MIT License. See license-bootstrap file.
JQuery is used in this project and is licensed under the MIT License. See license-jquery file.
Chart.js is used in this project and is licensed under the MIT License. See license-chartjs file.
Highlight.js is used in this project and is licensed under the BSD 3-clause License. See license-highlightjs file.
## requirements
- 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
You can install it in the following ways:
- download the latest release from the **"Releases"** section here
```bash
tar -xzf jilo-web_*.tgz
cd jilo-web/doc/
./install.sh
```
- clone the **git repo**:
```bash
git clone https://github.com/lindeas/jilo-web.git
cd jilo-web
./install.sh
```
- DEB and RPM packages are planned, but not yet available. There are experimental build scripts in "packaging" folder.
## config
- edit jilo-web.conf.php and set all the variables correctly
- "database" is the sqlite db file for jilo-web itself, create it with `cat jilo-web.schema | sqlite3 jilo-web.db`
- "sqlite_file" is the sqlite db file for jilo-web itself, it goes to `app/jilo-web.db`, create it with
```bash
cd app
cat ../doc/jilo-web.schema | sqlite3 jilo-web.db
```
- "jilo_database" is the sqlite db file for jilo, with data from the Jitsi logs
## database
The database is an SQLite file. You also need the sqlite db from jilo (mysql/mariadb support is planned, but not yet available).

27
TODO.md
View File

@ -1,27 +0,0 @@
# Jilo Web
## TODO
- ~~jilo-web.db outside web root~~
- ~~jilo-web.db writable by web server user~~
- major refactoring after v0.1
- - add bootstrap template
- - clean up the code to follow model-view--controller style
- - no HTML inside PHP code
- - put all additional functions in files in a separate folder
- - reduce try/catch usage, use it only for critical errors
- - move all SQL code in the model classes, one query per method
- - add 'limit' to SQL and make pagination
- - pretty URLs routing (no htaccess, it needs to be in PHP code so that it can be used with all servers)
- add mysql/mariadb option

View File

@ -0,0 +1,647 @@
<?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();
}
/**
* Retrieves details of agents for a specified host.
*
* @param int $host_id The host ID to filter agents by.
* @param int $agent_id Optional agent ID to filter by.
*
* @return array The list of agent details.
*/
public function getAgentDetails($host_id, $agent_id = '') {
$sql = 'SELECT
ja.id,
ja.host_id,
ja.agent_type_id,
ja.url,
ja.secret_key,
ja.check_period,
jat.description AS agent_description,
jat.endpoint AS agent_endpoint,
h.platform_id
FROM
jilo_agent ja
JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN
host h ON ja.host_id = h.id
WHERE
ja.host_id = :host_id';
if ($agent_id !== '') {
$sql .= ' AND ja.id = :agent_id';
}
$query = $this->db->prepare($sql);
$query->bindParam(':host_id', $host_id);
if ($agent_id !== '') {
$query->bindParam(':agent_id', $agent_id);
}
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 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.host_id,
ja.agent_type_id,
ja.url,
ja.secret_key,
ja.check_period,
jat.description AS agent_description,
jat.endpoint AS agent_endpoint,
h.platform_id
FROM
jilo_agent ja
JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN
host h ON ja.host_id = h.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_type
ORDER BY id';
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Retrieves agent types already configured for a specific host.
*
* @param int $host_id The host ID to filter agents by.
*
* @return array List of agent types configured for the host.
*/
public function getHostAgentTypes($host_id) {
$sql = 'SELECT
id,
agent_type_id
FROM
jilo_agent
WHERE
host_id = :host_id';
$query = $this->db->prepare($sql);
$query->bindParam(':host_id', $host_id);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Add a new agent to the database.
*
* @param int $host_id The host ID to add the agent to.
* @param array $newAgent An associative array containing the details of the agent to be added.
*
* @return bool|string True if the agent was added successfully, otherwise error message.
*/
public function addAgent($host_id, $newAgent) {
try {
$sql = 'INSERT INTO jilo_agent
(host_id, agent_type_id, url, secret_key, check_period)
VALUES
(:host_id, :agent_type_id, :url, :secret_key, :check_period)';
$query = $this->db->prepare($sql);
$query->execute([
':host_id' => $host_id,
':agent_type_id' => $newAgent['type_id'],
':url' => $newAgent['url'],
':secret_key' => $newAgent['secret_key'],
':check_period' => $newAgent['check_period'],
]);
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* Edit an existing agent in the database.
*
* @param int $agent_id The ID of the agent to edit.
* @param array $updatedAgent An associative array containing the updated details of the agent.
*
* @return bool|string True if the agent was updated successfully, otherwise error message.
*/
public function editAgent($agent_id, $updatedAgent) {
try {
$sql = 'UPDATE jilo_agent
SET
agent_type_id = :agent_type_id,
url = :url,
secret_key = :secret_key,
check_period = :check_period
WHERE
id = :agent_id';
// Convert empty secret key to NULL
$secretKey = !empty($updatedAgent['secret_key']) ? $updatedAgent['secret_key'] : null;
$query = $this->db->prepare($sql);
$query->execute([
':agent_id' => $agent_id,
':agent_type_id' => $updatedAgent['agent_type_id'],
':url' => $updatedAgent['url'],
':secret_key' => $secretKey,
':check_period' => $updatedAgent['check_period'],
]);
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* Deletes an agent from the database.
*
* @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_agent
WHERE
id = :agent_id';
$query = $this->db->prepare($sql);
$query->bindParam(':agent_id', $agent_id);
$query->execute();
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* 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';
return isset($_SESSION[$agent_cache_name]) && isset($_SESSION[$agent_cache_time]) && (time() - $_SESSION[$agent_cache_time] < 600);
}
/**
* 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), '+/', '-_'), '=');
}
/**
* 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
$header = json_encode([
'typ' => 'JWT',
'alg' => 'HS256'
]);
$base64Url_header = $this->base64UrlEncode($header);
// payload
$payload = json_encode($payload);
$base64Url_payload = $this->base64UrlEncode($payload);
// signature
$signature = hash_hmac('sha256', $base64Url_header . "." . $base64Url_payload, $secret_key ?? '', true);
$base64Url_signature = $this->base64UrlEncode($signature);
// build the JWT
$jwt = $base64Url_header . "." . $base64Url_payload . "." . $base64Url_signature;
return $jwt;
}
/**
* 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
$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)) {
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['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);
// 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
// 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;
}
/**
* 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"] = '';
}
/**
* 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 host, agent type, and metric type.
*
* @param int $host_id The host ID.
* @param string $agent_type The agent type.
* @param string $metric_type The metric type to filter by.
*
* @return mixed The latest stored data.
*/
public function getLatestData($host_id, $agent_type, $metric_type) {
$sql = 'SELECT
jac.timestamp,
jac.response_content,
jac.agent_id,
jat.description
FROM
jilo_agent_check jac
JOIN
jilo_agent ja ON jac.agent_id = ja.id
JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN
host h ON ja.host_id = h.id
WHERE
h.id = :host_id
AND jat.description = :agent_type
AND jac.status_code = 200
ORDER BY
jac.timestamp DESC
LIMIT 1';
$query = $this->db->prepare($sql);
$query->execute([
':host_id' => $host_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 $host_id The host ID
* @param string $agent_type The type of agent (e.g., 'jvb', 'jicofo')
* @param string $metric_type The type of metric to retrieve
* @param string $from_time Start time in Y-m-d format
* @param string $until_time End time in Y-m-d format
* @return array Array with the dataset from agent checks
*/
public function getHistoricalData($host_id, $agent_type, $metric_type, $from_time, $until_time) {
// Get data from agent checks
$sql = 'SELECT
DATE(jac.timestamp) as date,
jac.response_content,
COUNT(*) as checks_count
FROM
jilo_agent_check jac
JOIN
jilo_agent ja ON jac.agent_id = ja.id
JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN
host h ON ja.host_id = h.id
WHERE
h.id = :host_id
AND jat.description = :agent_type
AND jac.status_code = 200
AND 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([
':host_id' => $host_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;
}
/**
* Gets the previous record for a specific metric
*
* @param int $host_id The host ID
* @param string $agent_type The type of agent (e.g., 'jvb', 'jicofo')
* @param string $metric_type The type of metric to retrieve
* @param string $current_timestamp Current record's timestamp to get data before this
* @return array|null Previous record data or null if not found
*/
public function getPreviousRecord($host_id, $agent_type, $metric_type, $current_timestamp) {
$sql = 'SELECT
jac.timestamp,
jac.response_content
FROM
jilo_agent_check jac
JOIN
jilo_agent ja ON jac.agent_id = ja.id
JOIN
jilo_agent_type jat ON ja.agent_type_id = jat.id
JOIN
host h ON ja.host_id = h.id
WHERE
h.id = :host_id
AND jat.description = :agent_type
AND jac.status_code = 200
AND jac.timestamp < :current_timestamp
ORDER BY
jac.timestamp DESC
LIMIT 1';
$query = $this->db->prepare($sql);
$query->execute([
':host_id' => $host_id,
':agent_type' => $agent_type,
':current_timestamp' => $current_timestamp
]);
$result = $query->fetch(PDO::FETCH_ASSOC);
if ($result) {
$json_data = json_decode($result['response_content'], true);
if (json_last_error() === JSON_ERROR_NONE) {
$api_data = [];
if ($agent_type === 'jvb') {
$api_data = $json_data['jvb_api_data'] ?? [];
} elseif ($agent_type === 'jicofo') {
$api_data = $json_data['jicofo_api_data'] ?? [];
} elseif ($agent_type === 'jigasi') {
$api_data = $json_data['jigasi_api_data'] ?? [];
} elseif ($agent_type === 'prosody') {
$api_data = $json_data['prosody_api_data'] ?? [];
} elseif ($agent_type === 'nginx') {
$api_data = $json_data['nginx_api_data'] ?? [];
}
$value = $this->getNestedValue($api_data, $metric_type);
if ($value !== null) {
return [
'value' => $value,
'timestamp' => $result['timestamp']
];
}
}
}
return null;
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* API Response Handler
* Provides a consistent way to send JSON responses from controllers
*/
class ApiResponse {
/**
* Send a success response
* @param mixed $data Optional data to include in response
* @param string $message Optional success message
* @param int $status HTTP status code
*/
public static function success($data = null, $message = '', $status = 200) {
self::send([
'success' => true,
'data' => $data,
'message' => $message
], $status);
}
/**
* Send an error response
* @param string $message Error message
* @param mixed $errors Optional error details
* @param int $status HTTP status code
*/
public static function error($message, $errors = null, $status = 400) {
self::send([
'success' => false,
'error' => $message,
'errors' => $errors
], $status);
}
/**
* Send the actual JSON response
* @param array $data Response data
* @param int $status HTTP status code
*/
private static function send($data, $status) {
while (ob_get_level() > 0) {
ob_end_clean();
}
http_response_code($status);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
}

View File

@ -0,0 +1,169 @@
<?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();
}
/**
* 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='') {
global $logObject;
try {
// Add time part to dates if not present
if (strlen($from_time) <= 10) {
$from_time .= ' 00:00:00';
}
if (strlen($until_time) <= 10) {
$until_time .= ' 23:59:59';
}
// list of jitsi component events
$sql = "SELECT jitsi_component, loglevel, time, component_id, event_type, event_param
FROM jitsi_components
WHERE time >= :from_time
AND time <= :until_time";
// Only add component and event filters if they're not the default values
if ($jitsi_component !== 'jitsi_component') {
$sql .= " AND LOWER(jitsi_component) = LOWER(:jitsi_component)";
}
if ($component_id !== 'component_id') {
$sql .= " AND component_id = :component_id";
}
if ($event_type !== 'event_type') {
$sql .= " AND event_type LIKE :event_type";
}
$sql .= " ORDER BY time";
if ($items_per_page) {
$sql .= ' LIMIT :offset, :items_per_page';
}
$stmt = $this->db->prepare($sql);
// Bind parameters only if they're not default values
if ($jitsi_component !== 'jitsi_component') {
$stmt->bindValue(':jitsi_component', trim($jitsi_component, "'"));
}
if ($component_id !== 'component_id') {
$stmt->bindValue(':component_id', trim($component_id, "'"));
}
if ($event_type !== 'event_type') {
$stmt->bindValue(':event_type', '%' . trim($event_type, "'") . '%');
}
$stmt->bindParam(':from_time', $from_time);
$stmt->bindParam(':until_time', $until_time);
if ($items_per_page) {
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->bindParam(':items_per_page', $items_per_page, PDO::PARAM_INT);
}
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($result)) {
$logObject->log('info', "Retrieved " . count($result) . " Jitsi component events", ['user_id' => $userId, 'scope' => 'system']);
}
return $result;
} catch (PDOException $e) {
$logObject->log('error', "Failed to retrieve Jitsi component events: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
return [];
}
}
/**
* Gets the total count of components events matching the filter criteria
*
* @param string $jitsi_component The Jitsi component name.
* @param int $component_id The component ID.
* @param string $event_type The type of event to filter by.
* @param string $from_time The start date in 'YYYY-MM-DD' format.
* @param string $until_time The end date in 'YYYY-MM-DD' format.
*
* @return int The total count of matching components
*/
public function getComponentEventsCount($jitsi_component, $component_id, $event_type, $from_time, $until_time) {
global $logObject;
try {
// Add time part to dates if not present
if (strlen($from_time) <= 10) {
$from_time .= ' 00:00:00';
}
if (strlen($until_time) <= 10) {
$until_time .= ' 23:59:59';
}
// Build the query
$sql = "SELECT COUNT(*) as total
FROM jitsi_components
WHERE time >= :from_time
AND time <= :until_time";
// Only add component and event filters if they're not the default values
if ($jitsi_component !== 'jitsi_component') {
$sql .= " AND LOWER(jitsi_component) = LOWER(:jitsi_component)";
}
if ($component_id !== 'component_id') {
$sql .= " AND component_id = :component_id";
}
if ($event_type !== 'event_type') {
$sql .= " AND event_type LIKE :event_type";
}
$stmt = $this->db->prepare($sql);
// Bind parameters only if they're not default values
if ($jitsi_component !== 'jitsi_component') {
$stmt->bindValue(':jitsi_component', trim($jitsi_component, "'"));
}
if ($component_id !== 'component_id') {
$stmt->bindValue(':component_id', trim($component_id, "'"));
}
if ($event_type !== 'event_type') {
$stmt->bindValue(':event_type', '%' . trim($event_type, "'") . '%');
}
$stmt->bindParam(':from_time', $from_time);
$stmt->bindParam(':until_time', $until_time);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return (int)$result['total'];
} catch (PDOException $e) {
$logObject->log('error', "Failed to retrieve component events count: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
return 0;
}
}
}

View File

@ -0,0 +1,384 @@
<?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();
}
/**
* 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
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// search for a conference by its ID for a time period (if given)
$sql = "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
c.conference_id = '%s'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
c.conference_id = '%s'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time";
if ($items_per_page) {
$items_per_page = (int)$items_per_page;
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
}
$sql = sprintf($sql, $conference_id, $from_time, $until_time, $conference_id, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 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
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// search for a conference by its name for a time period (if given)
$sql = "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
c.conference_name = '%s'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
c.conference_name = '%s'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time";
if ($items_per_page) {
$items_per_page = (int)$items_per_page;
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
}
$sql = sprintf($sql, $conference_name, $from_time, $until_time, $conference_name, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 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
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// list of conferences for time period (if given)
// fields: component, duration, conference ID, conference name, number of participants, name count (the conf name is found), conference host
$sql = "
SELECT DISTINCT
c.jitsi_component,
(SELECT COALESCE
(
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
ce.conference_event = 'conference created'
),
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
ce.conference_event = 'bridge selected'
)
)
)
AS start,
(SELECT COALESCE
(
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
(ce.conference_event = 'conference expired' OR ce.conference_event = 'conference stopped')
),
(SELECT pe.time
FROM participant_events pe
WHERE
pe.event_param = c.conference_id
ORDER BY pe.time DESC
LIMIT 1
)
)
)
AS end,
c.conference_id,
c.conference_name,
(SELECT COUNT(pe.participant_id)
FROM participant_events pe
WHERE
pe.event_type = 'participant joining'
AND
pe.event_param = c.conference_id) AS participants,
name_counts.name_count,
c.conference_host
FROM
conferences c
JOIN (
SELECT
conference_name,
COUNT(*) AS name_count
FROM
conferences
GROUP BY
conference_name
) AS name_counts ON c.conference_name = name_counts.conference_name
JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE (ce.time >= '%s 00:00:00' AND ce.time <= '%s 23:59:59')
ORDER BY
c.id";
if ($items_per_page) {
$items_per_page = (int)$items_per_page;
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
}
$sql = sprintf($sql, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 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
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// number of conferences for time period (if given)
// FIXME sometimes there is no start/end time, find a way around this
$sql = "
SELECT COUNT(*) AS conferences
FROM (
SELECT DISTINCT
(SELECT COALESCE
(
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
ce.conference_event = 'conference created'
),
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
ce.conference_event = 'bridge selected'
)
)
) AS start,
(SELECT COALESCE
(
(SELECT ce.time
FROM conference_events ce
WHERE
ce.conference_id = c.conference_id
AND
(ce.conference_event = 'conference expired' OR ce.conference_event = 'conference stopped')
),
(SELECT pe.time
FROM participant_events pe
WHERE
pe.event_param = c.conference_id
ORDER BY pe.time DESC
LIMIT 1
)
)
) AS end
FROM conferences c
JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE (start >= '%s 00:00:00' AND end <= '%s 23:59:59')
) AS subquery";
$sql = sprintf($sql, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
}

View File

@ -0,0 +1,158 @@
<?php
/**
* class Config
*
* Handles editing and fetching of the config files.
*/
class Config {
/**
* Edits a config file by updating specified options.
*
* @param array $updatedConfig Key-value pairs of config options to update.
* @param string $config_file Path to the config file.
*
* @return array Returns an array with 'success' and 'updated' keys on success, or 'success' and 'error' keys on failure.
*/
public function editConfigFile($updatedConfig, $config_file) {
global $logObject, $userId;
$allLogs = [];
$updated = [];
try {
if (!is_array($updatedConfig)) {
throw new Exception("Invalid config data: expected array");
}
if (!file_exists($config_file) || !is_writable($config_file)) {
throw new Exception("Config file does not exist or is not writable: $config_file");
}
// First we get a fresh config file contents as text
$config_contents = file_get_contents($config_file);
if ($config_contents === false) {
throw new Exception("Failed to read the config file: $config_file");
}
$lines = explode("\n", $config_contents);
// We loop through the variables and update them
foreach ($updatedConfig as $key => $newValue) {
if (strpos($key, '[') !== false) {
preg_match_all('/([^\[\]]+)/', $key, $matches);
if (empty($matches[1])) continue;
$parts = $matches[1];
$currentPath = [];
$found = false;
$inTargetArray = false;
foreach ($lines as $i => $line) {
$line = rtrim($line);
if (preg_match("/^\\s*\\]/", $line)) {
if (!empty($currentPath)) {
if ($inTargetArray && end($currentPath) === $parts[0]) {
$inTargetArray = false;
}
array_pop($currentPath);
}
continue;
}
if (preg_match("/^\\s*['\"]([^'\"]+)['\"]\\s*=>/", $line, $matches)) {
$key = $matches[1];
if (strpos($line, '[') !== false) {
$currentPath[] = $key;
if ($key === $parts[0]) {
$inTargetArray = true;
}
} else if ($key === end($parts) && $inTargetArray) {
$pathMatches = true;
$expectedPath = array_slice($parts, 0, -1);
if (count($currentPath) === count($expectedPath)) {
for ($j = 0; $j < count($expectedPath); $j++) {
if ($currentPath[$j] !== $expectedPath[$j]) {
$pathMatches = false;
break;
}
}
if ($pathMatches) {
if ($newValue === 'true' || $newValue === '1') {
$replacementValue = 'true';
} elseif ($newValue === 'false' || $newValue === '0') {
$replacementValue = 'false';
} else {
$replacementValue = var_export($newValue, true);
}
if (preg_match("/^(\\s*['\"]" . preg_quote($key, '/') . "['\"]\\s*=>\\s*).*?(,?)\\s*$/", $line, $matches)) {
$lines[$i] = $matches[1] . $replacementValue . $matches[2];
$updated[] = implode('.', array_merge($currentPath, [$key]));
$found = true;
}
}
}
}
}
}
if (!$found) {
$allLogs[] = "Failed to update: $key";
}
} else {
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $key)) {
throw new Exception("Invalid config key format: $key");
}
if ($newValue === 'true' || $newValue === '1') {
$replacementValue = 'true';
} elseif ($newValue === 'false' || $newValue === '0') {
$replacementValue = 'false';
} else {
$replacementValue = var_export($newValue, true);
}
$found = false;
foreach ($lines as $i => $line) {
if (preg_match("/^(\\s*['\"]" . preg_quote($key, '/') . "['\"]\\s*=>\\s*).*?(,?)\\s*$/", $line, $matches)) {
$lines[$i] = $matches[1] . $replacementValue . $matches[2];
$updated[] = $key;
$found = true;
break;
}
}
if (!$found) {
$allLogs[] = "Failed to update: $key";
}
}
}
// We write the new config file
$new_contents = implode("\n", $lines);
if (file_put_contents($config_file, $new_contents) === false) {
throw new Exception("Failed to write the config file: $config_file");
}
if (!empty($allLogs)) {
$logObject->log('info', implode("\n", $allLogs), ['user_id' => $userId, 'scope' => 'system']);
}
return [
'success' => true,
'updated' => $updated
];
} catch (Exception $e) {
$logObject->log('error', "Config update error: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
}

View File

@ -0,0 +1,223 @@
<?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) {
// check if PDO extension is loaded
if (!extension_loaded('pdo')) {
throw new Exception('PDO extension not loaded.');
}
// options check
if (empty($options['type'])) {
throw new Exception('Database type is not set.');
}
// connect based on database type
switch ($options['type']) {
case 'sqlite':
$this->connectSqlite($options);
break;
case 'mysql':
case 'mariadb':
$this->connectMysql($options);
break;
default:
$this->pdo = null;
}
}
/**
* 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')) {
throw new Exception('PDO extension for SQLite not loaded.');
}
// SQLite options
if (empty($options['dbFile'])) {
throw new Exception('SQLite database file path is missing.');
}
// For in-memory database (especially for the tests), skip file check
if ($options['dbFile'] !== ':memory:' && !file_exists($options['dbFile'])) {
throw new Exception("SQLite database file \"{$options['dbFile']}\" not found.");
}
// connect to SQLite
try {
$this->pdo = new PDO("sqlite:" . $options['dbFile']);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// enable foreign key constraints (not ON by default in SQLite3)
$this->pdo->exec('PRAGMA foreign_keys = ON;');
} catch (PDOException $e) {
throw new Exception('SQLite connection failed: ' . $e->getMessage());
}
}
/**
* 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')) {
throw new Exception('PDO extension for MySQL not loaded.');
}
// MySQL options
if (empty($options['host']) || empty($options['dbname']) || empty($options['user'])) {
throw new Exception('MySQL connection data is missing.');
}
// Connect to MySQL
try {
$port = $options['port'] ?? 3306;
$dsn = "mysql:host={$options['host']};port={$port};dbname={$options['dbname']};charset=utf8";
$this->pdo = new PDO($dsn, $options['user'], $options['password'] ?? '');
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
$this->pdo = null;
}
}
/**
* 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;
}
/**
* Executes an SQL query with optional parameters.
*
* @param string $query The SQL query to execute
* @param array $params Optional parameters for the query
* @return PDOStatement|false The result of the query execution
* @throws Exception If the query fails
*/
public function execute($query, $params = []) {
if (!$this->pdo) {
throw new Exception('No database connection.');
}
try {
$stmt = $this->pdo->prepare($query);
$stmt->execute($params);
return $stmt;
} catch (PDOException $e) {
throw new Exception('Query execution failed: ' . $e->getMessage());
}
}
/**
* Prepares an SQL statement for execution.
*
* @param string $query The SQL query to prepare
* @return PDOStatement The prepared statement
* @throws Exception If the preparation fails
*/
public function prepare($query) {
if (!$this->pdo) {
throw new Exception('No database connection.');
}
try {
return $this->pdo->prepare($query);
} catch (PDOException $e) {
throw new Exception('Statement preparation failed: ' . $e->getMessage());
}
}
/**
* Begins a database transaction.
*
* @throws Exception If starting the transaction fails
*/
public function beginTransaction() {
if (!$this->pdo) {
throw new Exception('No database connection.');
}
try {
return $this->pdo->beginTransaction();
} catch (PDOException $e) {
throw new Exception('Failed to start transaction: ' . $e->getMessage());
}
}
/**
* Commits the current database transaction.
*
* @throws Exception If committing the transaction fails
*/
public function commit() {
if (!$this->pdo) {
throw new Exception('No database connection.');
}
try {
return $this->pdo->commit();
} catch (PDOException $e) {
throw new Exception('Failed to commit transaction: ' . $e->getMessage());
}
}
/**
* Rolls back the current database transaction.
*
* @throws Exception If rolling back the transaction fails
*/
public function rollBack() {
if (!$this->pdo) {
throw new Exception('No database connection.');
}
try {
return $this->pdo->rollBack();
} catch (PDOException $e) {
throw new Exception('Failed to rollback transaction: ' . $e->getMessage());
}
}
}

View File

@ -0,0 +1,247 @@
<?php
class Feedback {
// Feedback types
const TYPE_SUCCESS = 'success';
const TYPE_ERROR = 'danger';
const TYPE_INFO = 'info';
const TYPE_WARNING = 'warning';
// Default feedback 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
],
'SESSION_TIMEOUT' => [
'type' => self::TYPE_ERROR,
'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 REGISTER = [
'SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'FAILED' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
],
'DISABLED' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
];
const SECURITY = [
'WHITELIST_ADD_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'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
]
];
const THEME = [
'THEME_CHANGE_SUCCESS' => [
'type' => self::TYPE_SUCCESS,
'dismissible' => true
],
'THEME_CHANGE_FAILED' => [
'type' => self::TYPE_ERROR,
'dismissible' => true
]
];
const SYSTEM = [
'DB_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'DB_CONNECT_ERROR' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'DB_UNKNOWN_TYPE' => [
'type' => self::TYPE_ERROR,
'dismissible' => false
],
'MIGRATIONS_PENDING' => [
'type' => self::TYPE_WARNING,
'dismissible' => true
],
'MAINTENANCE_ON' => [
'type' => self::TYPE_WARNING,
'dismissible' => false
],
];
private static $strings = null;
/**
* Get feedback message strings
*/
private static function getStrings() {
if (self::$strings === null) {
self::$strings = require __DIR__ . '/../includes/strings.php';
}
return self::$strings;
}
/**
* Get feedback 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 feedback message HTML
*/
// Usage: echo Feedback::render('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
public static function render($category, $key, $customMessage = null, $dismissible = null, $small = false, $sanitize = true) {
$config = self::get($category, $key);
if (!$config) return '';
$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
);
}
/**
* Get feedback message data for JavaScript
*/
public static function getMessageData($category, $key, $customMessage = null, $dismissible = null, $small = false) {
$config = self::get($category, $key);
if (!$config) return null;
return [
'type' => $config['type'],
'message' => $customMessage ?? $config['message'],
'dismissible' => $dismissible ?? $config['dismissible'] ?? false,
'small' => $small
];
}
/**
* Store feedback message in session for display after redirect
*/
// Usage: Feedback::flash('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
public static function flash($category, $key, $customMessage = null, $dismissible = null, $small = false, $sanitize = true) {
if (!isset($_SESSION['flash_messages'])) {
$_SESSION['flash_messages'] = [];
}
// Get the feedback 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,
'sanitize' => $sanitize
];
}
/**
* Get and clear all flash feedback messages
*/
public static function getFlash() {
$system_messages = $_SESSION['flash_messages'] ?? [];
unset($_SESSION['flash_messages']);
return $system_messages;
}
}

View File

@ -0,0 +1,165 @@
<?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,
platform_id,
name
FROM
host';
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 host
(address, platform_id, name)
VALUES
(:address, :platform_id, :name)';
$query = $this->db->prepare($sql);
$query->execute([
':address' => $newHost['address'],
':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|string True if the host was updated successfully, otherwise error message.
*/
public function editHost($platform_id, $updatedHost) {
try {
$sql = 'UPDATE host SET
address = :address,
name = :name
WHERE
id = :id AND platform_id = :platform_id';
$query = $this->db->prepare($sql);
$query->execute([
':id' => $updatedHost['id'],
':platform_id' => $platform_id,
':address' => $updatedHost['address'],
':name' => $updatedHost['name']
]);
if ($query->rowCount() === 0) {
return "No host found with ID {$updatedHost['id']} in platform $platform_id";
}
return true;
} catch (Exception $e) {
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 {
// Start transaction
$this->db->beginTransaction();
// First delete all agents associated with this host
$sql = 'DELETE FROM jilo_agent WHERE host_id = :host_id';
$query = $this->db->prepare($sql);
$query->bindParam(':host_id', $host_id);
$query->execute();
// Then delete the host
$sql = 'DELETE FROM host WHERE id = :host_id';
$query = $this->db->prepare($sql);
$query->bindParam(':host_id', $host_id);
$query->execute();
// Commit transaction
$this->db->commit();
return true;
} catch (Exception $e) {
// Rollback transaction on error
$this->db->rollBack();
return $e->getMessage();
}
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* Log wrapper that delegates to plugin Log or NullLogger fallback.
* Used when code does require_once '../app/classes/log.php'.
*/
use App\Core\NullLogger;
// If there is already a Log plugin loaded
if (class_exists('Log')) {
return;
}
// Load fallback NullLogger
require_once __DIR__ . '/../core/NullLogger.php';
class Log {
private $logger;
/**
* @param mixed $database Database or DatabaseConnector instance
*/
public function __construct($database) {
global $logObject;
if (isset($logObject) && method_exists($logObject, 'insertLog')) {
$this->logger = $logObject;
} else {
$this->logger = new NullLogger();
}
}
/**
* PSR-3 compatible log method
* @param string $level
* @param string $message
* @param array $context
*/
public function log(string $level, string $message, array $context = []): void {
$this->logger->log($level, $message, $context);
}
}

View File

@ -0,0 +1,374 @@
<?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();
}
/**
* 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
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// list conferences where participant ID (endpoint_id) is found
$sql = "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
p.endpoint_id = '%s'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
participant_id = '%s'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time";
if ($items_per_page) {
$items_per_page = (int)$items_per_page;
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
}
$sql = sprintf($sql, $participant_id, $from_time, $until_time, $participant_id, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 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
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// list conferences where participant name (stats_id) is found
$sql = "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
pe.event_type = 'stats_id' AND pe.event_param LIKE '%%%s%%'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
event_type = 'stats_id' AND event_param LIKE '%%%s%%'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time";
if ($items_per_page) {
$items_per_page = (int)$items_per_page;
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
}
$sql = sprintf($sql, $participant_name, $from_time, $until_time, $participant_name, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 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
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// list conferences where participant IP is found
$sql = "
SELECT
pe.time,
c.conference_id,
c.conference_name,
c.conference_host,
pe.loglevel,
pe.event_type,
p.endpoint_id AS participant_id,
pe.event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
LEFT JOIN
participants p ON c.conference_id = p.conference_id
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
pe.event_type = 'pair selected' AND pe.event_param = '%s'
AND (pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
UNION
SELECT
ce.time AS event_time,
c.conference_id,
c.conference_name,
c.conference_host,
ce.loglevel,
ce.conference_event AS event_type,
NULL AS participant_id,
ce.conference_param AS event_param
FROM
conferences c
LEFT JOIN
conference_events ce ON c.conference_id = ce.conference_id
WHERE
event_type = 'pair selected' AND event_param = '%s'
AND (event_time >= '%s 00:00:00' AND event_time <= '%s 23:59:59')
ORDER BY
pe.time";
if ($items_per_page) {
$items_per_page = (int)$items_per_page;
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
}
$sql = sprintf($sql, $participant_ip, $from_time, $until_time, $participant_ip, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 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
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// list all participants
$sql = "
SELECT DISTINCT
p.jitsi_component, p.endpoint_id, p.conference_id
FROM
participants p
JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59'
ORDER BY p.id";
if ($items_per_page) {
$items_per_page = (int)$items_per_page;
$sql .= ' LIMIT ' . $offset . ',' . $items_per_page;
}
$sql = sprintf($sql, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 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
// FIXME make it similar to the bash version
if (empty($from_time)) {
$from_time = '0000-01-01';
}
if (empty($until_time)) {
$until_time = '9999-12-31';
}
// this is needed for compatibility with the bash version, so we use '%s' placeholders
$from_time = htmlspecialchars(strip_tags($from_time));
$until_time = htmlspecialchars(strip_tags($until_time));
// number of participants for time period (if given)
$sql = "
SELECT COUNT(DISTINCT p.endpoint_id) as participants
FROM
participants p
LEFT JOIN
participant_events pe ON p.endpoint_id = pe.participant_id
WHERE
(pe.time >= '%s 00:00:00' AND pe.time <= '%s 23:59:59')
AND pe.event_type = 'participant joining'";
$sql = sprintf($sql, $from_time, $until_time);
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
}

View File

@ -0,0 +1,167 @@
<?php
/**
* Handles password reset functionality including token generation and validation
*/
class PasswordReset {
private $db;
private const TOKEN_LENGTH = 32;
private const TOKEN_EXPIRY = 3600; // 1 hour
public function __construct($database) {
if ($database instanceof PDO) {
$this->db = $database;
} else {
$this->db = $database->getConnection();
}
}
/**
* Creates a password reset request and sends email to user
*
* @param string $email User's email address
* @return array Status of the reset request
*/
public function requestReset($email) {
// Check if email exists
$query = $this->db->prepare("
SELECT u.id, um.email
FROM user u
JOIN user_meta um ON u.id = um.user_id
WHERE um.email = :email"
);
$query->bindParam(':email', $email);
$query->execute();
$user = $query->fetch(PDO::FETCH_ASSOC);
if (!$user) {
return ['success' => false, 'message' => 'If this email exists in our system, you will receive reset instructions.'];
}
// Generate unique token
$token = bin2hex(random_bytes(self::TOKEN_LENGTH / 2));
$expires = time() + self::TOKEN_EXPIRY;
// Store token in database
$query = $this->db->prepare("
INSERT INTO user_password_reset (user_id, token, expires)
VALUES (:user_id, :token, :expires)"
);
$query->bindParam(':user_id', $user['id']);
$query->bindParam(':token', $token);
$query->bindParam(':expires', $expires);
if (!$query->execute()) {
return ['success' => false, 'message' => 'Failed to process reset request'];
}
// We need the config for the email details
global $config;
// Prepare the reset link
$scheme = $_SERVER['REQUEST_SCHEME'];
$domain = trim($config['domain'], '/');
$folder = trim($config['folder'], '/');
$folderPath = $folder !== '' ? "/$folder" : '';
$resetLink = "{$scheme}://{$domain}{$folderPath}/index.php?page=login&action=reset&token=" . urlencode($token);
// Send email with reset link
$to = $user['email'];
// Load email helper
require_once __DIR__ . '/../helpers/email_helper.php';
$subject = "{$config['site_name']} - Password reset request";
$variables = [
'site_name' => $config['site_name'],
'reset_link' => $resetLink,
'site_slogan' => $config['site_slogan'] ?? ''
];
$additionalHeaders = [
'From' => "noreply@{$config['domain']}",
'Reply-To' => "noreply@{$config['domain']}"
];
if (!sendTemplateEmail($to, $subject, 'password_reset', $variables, $config, $additionalHeaders)) {
return ['success' => false, 'message' => 'Failed to send reset email'];
}
return ['success' => true, 'message' => 'If this email exists in our system, you will receive reset instructions.'];
}
/**
* Validates a reset token and returns associated user ID if valid
*
* @param string $token Reset token
* @return array Validation result with user ID if successful
*/
public function validateToken($token) {
$now = time();
$query = $this->db->prepare("
SELECT user_id
FROM user_password_reset
WHERE token = :token
AND expires > :now
AND used = 0
");
$query->bindParam(':token', $token);
$query->bindParam(':now', $now);
$query->execute();
$result = $query->fetch(PDO::FETCH_ASSOC);
if (!$result) {
return ['valid' => false];
}
return ['valid' => true, 'user_id' => $result['user_id']];
}
/**
* Completes the password reset process
*
* @param string $token Reset token
* @param string $newPassword New password
* @return bool Whether reset was successful
*/
public function resetPassword($token, $newPassword) {
$validation = $this->validateToken($token);
if (!$validation['valid']) {
return false;
}
// Start transaction
$this->db->beginTransaction();
try {
// Update password
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
$query = $this->db->prepare(
"UPDATE user
SET password = :password
WHERE id = :user_id"
);
$query->bindParam(':password', $hashedPassword);
$query->bindParam(':user_id', $validation['user_id']);
$query->execute();
// Mark token as used
$query = $this->db->prepare(
"UPDATE user_password_reset
SET used = 1
WHERE token = :token"
);
$query->bindParam(':token', $token);
$query->execute();
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
return false;
}
}
}

View File

@ -0,0 +1,163 @@
<?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();
}
/**
* 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 platform';
if ($platform_id !== '') {
$sql .= ' WHERE id = :platform_id';
$query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id);
} else {
$query = $this->db->prepare($sql);
}
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 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 platform
(name, jitsi_url, jilo_database)
VALUES
(:name, :jitsi_url, :jilo_database)';
$query = $this->db->prepare($sql);
$query->execute([
':name' => $newPlatform['name'],
':jitsi_url' => $newPlatform['jitsi_url'],
':jilo_database' => $newPlatform['jilo_database'],
]);
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* 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 platform SET
name = :name,
jitsi_url = :jitsi_url,
jilo_database = :jilo_database
WHERE
id = :platform_id';
$query = $this->db->prepare($sql);
$query->execute([
':name' => $updatedPlatform['name'],
':jitsi_url' => $updatedPlatform['jitsi_url'],
':jilo_database' => $updatedPlatform['jilo_database'],
':platform_id' => $platform_id,
]);
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* 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 {
$this->db->beginTransaction();
// First, get all hosts in this platform
$sql = 'SELECT id FROM host WHERE platform_id = :platform_id';
$query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id);
$query->execute();
$hosts = $query->fetchAll(PDO::FETCH_ASSOC);
// Delete all agents for each host
foreach ($hosts as $host) {
$sql = 'DELETE FROM jilo_agent WHERE host_id = :host_id';
$query = $this->db->prepare($sql);
$query->bindParam(':host_id', $host['id']);
$query->execute();
}
// Delete all hosts in this platform
$sql = 'DELETE FROM host WHERE platform_id = :platform_id';
$query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id);
$query->execute();
// Finally, delete the platform
$sql = 'DELETE FROM platform WHERE id = :platform_id';
$query = $this->db->prepare($sql);
$query->bindParam(':platform_id', $platform_id);
$query->execute();
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
return $e->getMessage();
}
}
}

View File

@ -0,0 +1,669 @@
<?php
use App\App;
use App\Core\NullLogger;
class RateLimiter {
public $db;
/** @var mixed NullLogger (or PSR-3 logger) or plugin Log */
private $logger;
public $maxAttempts = 5; // Maximum login attempts
public $decayMinutes = 15; // Time window in minutes
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
public $autoBlacklistDuration = 24; // Hours to blacklist for
public $authRatelimitTable = 'security_rate_auth'; // For rate limiting username/password attempts
public $pagesRatelimitTable = 'security_rate_page'; // For rate limiting page requests
public $whitelistTable = 'security_ip_whitelist'; // For whitelisting IPs and network ranges
public $blacklistTable = 'security_ip_blacklist'; // For blacklisting IPs and network ranges
private $pageLimits = [
// Default rate limits per minute
'default' => 60,
'admin' => 120,
'message' => 20,
'contact' => 30,
'call' => 30,
'register' => 5,
'config' => 10
];
/**
* @param mixed $logger Optional NullLogger (or PSR-3 logger) or plugin Log
*/
public function __construct($logger = null) {
$db = App::db();
// Extract PDO connection from Database object
$this->db = ($db instanceof PDO) ? $db : $db->getConnection();
// Initialize logger (plugin Log if present or NullLogger otherwise)
if ($logger !== null) {
$this->logger = $logger;
} else {
global $logObject;
$this->logger = isset($logObject) && is_object($logObject) && method_exists($logObject, 'info')
? $logObject
: new NullLogger();
}
// Initialize database tables
$this->createTablesIfNotExist();
}
// Database preparation
private function createTablesIfNotExist() {
// Authentication attempts table
$sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
id int(11) PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
username VARCHAR(255) NOT NULL,
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip_username (ip_address, username)
)";
$this->db->exec($sql);
// Pages rate limits table
$sql = "CREATE TABLE IF NOT EXISTS {$this->pagesRatelimitTable} (
id int(11) PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
endpoint VARCHAR(255) NOT NULL,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip_endpoint (ip_address, endpoint),
INDEX idx_request_time (request_time)
)";
$this->db->exec($sql);
// IP whitelist table
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
id int(11) PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
is_network BOOLEAN DEFAULT FALSE,
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255),
UNIQUE KEY unique_ip (ip_address)
)";
$this->db->exec($sql);
// IP blacklist table
$sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} (
id int(11) PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
is_network BOOLEAN DEFAULT FALSE,
reason VARCHAR(255),
expiry_time TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255),
UNIQUE KEY unique_ip (ip_address)
)";
$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 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 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->authRatelimitTable}
WHERE ip_address = ? AND attempted_at > DATE_SUB(NOW(), INTERVAL ? MINUTE)");
$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} WHERE ip_address = ?");
$stmt->execute([$ip]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
// Skip expired entries
if ($row['expiry_time'] !== null && strtotime($row['expiry_time']) < time()) {
return false;
}
return true;
}
// Check network ranges
$stmt = $this->db->prepare("SELECT ip_address, expiry_time FROM {$this->blacklistTable} WHERE is_network = 1");
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// Skip expired entries
if ($row['expiry_time'] !== null && strtotime($row['expiry_time']) < time()) {
continue;
}
if ($this->ipInRange($ip, $row['ip_address'])) {
return true;
}
}
return false;
}
/**
* Check if an IP is whitelisted
*/
public function isIpWhitelisted($ip) {
// Check exact IP match first
$stmt = $this->db->prepare("SELECT ip_address FROM {$this->whitelistTable} WHERE ip_address = ?");
$stmt->execute([$ip]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
return true;
}
// Only check ranges for IPv4 addresses
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
// Check network ranges
$stmt = $this->db->prepare("SELECT ip_address FROM {$this->whitelistTable} WHERE is_network = 1");
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
if ($this->ipInRange($ip, $row['ip_address'])) {
return true;
}
}
}
return false;
}
private function ipInRange($ip, $cidr) {
// Only work with IPv4 addresses
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return false;
}
list($subnet, $bits) = explode('/', $cidr);
// Make sure subnet is IPv4
if (!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return false;
}
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$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->logger->log('info', "IP Whitelist: {$message}", ['user_id' => $userId, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', $message);
}
return false;
}
$stmt = $this->db->prepare("INSERT INTO {$this->whitelistTable}
(ip_address, is_network, description, created_by)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
is_network = VALUES(is_network),
description = VALUES(description),
created_by = VALUES(created_by)");
$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->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
}
return $result;
} catch (Exception $e) {
if ($userId) {
$this->logger->log('error', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage());
}
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->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
}
return $result;
} catch (Exception $e) {
if ($userId) {
$this->logger->log('error', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage());
}
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->logger->log('info', "IP Blacklist: {$message}", ['user_id' => $userId, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', $message);
}
return false;
}
$expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null;
$stmt = $this->db->prepare("INSERT INTO {$this->blacklistTable}
(ip_address, is_network, reason, expiry_time, created_by)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
is_network = VALUES(is_network),
reason = VALUES(reason),
expiry_time = VALUES(expiry_time),
created_by = VALUES(created_by)");
$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->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
}
return $result;
} catch (Exception $e) {
if ($userId) {
$this->logger->log('error', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage());
}
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->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']);
}
return $result;
} catch (Exception $e) {
if ($userId) {
$this->logger->log('error', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage());
}
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 < NOW()");
$stmt->execute();
// Clean old login attempts
$stmt = $this->db->prepare("DELETE FROM {$this->authRatelimitTable}
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)");
$stmt->execute([':minutes' => $this->decayMinutes]);
return true;
} catch (Exception $e) {
$this->logger->log('error', "Failed to cleanup expired entries: " . $e->getMessage(), ['user_id' => $userId ?? null, 'scope' => 'system']);
Feedback::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage());
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->authRatelimitTable}
WHERE ip_address = :ip
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$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, $failed = true) {
// Only record failed attempts
if (!$failed) {
return true;
}
// Record this attempt
$sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)";
$stmt = $this->db->prepare($sql);
try {
$stmt->execute([
':ip' => $ipAddress,
':username' => $username
]);
} catch (PDOException $e) {
return false;
}
return true;
}
public function tooManyAttempts($username, $ipAddress) {
$sql = "SELECT COUNT(*) as attempts
FROM {$this->authRatelimitTable}
WHERE ip_address = :ip
AND username = :username
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':username' => $username,
':minutes' => $this->decayMinutes
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
// Also check what's in the table
$sql = "SELECT * FROM {$this->authRatelimitTable} WHERE ip_address = :ip";
$stmt = $this->db->prepare($sql);
$stmt->execute([':ip' => $ipAddress]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$tooMany = $result['attempts'] >= $this->maxAttempts;
// Auto-blacklist if too many attempts
if ($tooMany) {
$this->addToBlacklist(
$ipAddress,
false,
'Auto-blacklisted due to excessive login attempts',
'system',
null,
$this->autoBlacklistDuration
);
}
return $tooMany;
}
public function clearOldAttempts() {
$sql = "DELETE FROM {$this->authRatelimitTable}
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':minutes' => $this->decayMinutes
]);
}
public function getRemainingAttempts($username, $ipAddress) {
$sql = "SELECT COUNT(*) as attempts
FROM {$this->authRatelimitTable}
WHERE ip_address = :ip
AND username = :username
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$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;
}
/**
* Check if a page request is allowed
*/
public function isPageRequestAllowed($ipAddress, $endpoint, $userId = null) {
// First check if IP is blacklisted
if ($this->isIpBlacklisted($ipAddress)) {
return false;
}
// Then check if IP is whitelisted
if ($this->isIpWhitelisted($ipAddress)) {
return true;
}
// Clean old requests
$this->cleanOldPageRequests();
// Get limit based on endpoint type and user role
$limit = $this->getPageLimitForEndpoint($endpoint, $userId);
// Count recent requests, including this one
$sql = "SELECT COUNT(*) as request_count
FROM {$this->pagesRatelimitTable}
WHERE ip_address = :ip
AND endpoint = :endpoint
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':endpoint' => $endpoint
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result['request_count'] < $limit;
}
/**
* Record a page request
*/
public function recordPageRequest($ipAddress, $endpoint) {
$sql = "INSERT INTO {$this->pagesRatelimitTable} (ip_address, endpoint)
VALUES (:ip, :endpoint)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':ip' => $ipAddress,
':endpoint' => $endpoint
]);
}
/**
* Clean old page requests
*/
private function cleanOldPageRequests() {
$sql = "DELETE FROM {$this->pagesRatelimitTable}
WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
$stmt = $this->db->prepare($sql);
$stmt->execute();
}
/**
* Get page rate limit for endpoint
*/
private function getPageLimitForEndpoint($endpoint, $userId = null) {
// Admin users get higher limits
if ($userId) {
// Check admin rights directly from database
$stmt = $this->db->prepare('SELECT COUNT(*) FROM `user_right` ur JOIN `right` r ON ur.right_id = r.id WHERE ur.user_id = ? AND r.name = ?');
$stmt->execute([$userId, 'superuser']);
if ($stmt->fetchColumn() > 0) {
return $this->pageLimits['admin'];
}
}
// Get endpoint type from the endpoint path
$endpointType = $this->getEndpointType($endpoint);
// Return specific limit if exists, otherwise default
return isset($this->pageLimits[$endpointType])
? $this->pageLimits[$endpointType]
: $this->pageLimits['default'];
}
/**
* Get endpoint type from path
*/
private function getEndpointType($endpoint) {
if (strpos($endpoint, 'message') !== false) return 'message';
if (strpos($endpoint, 'contact') !== false) return 'contact';
if (strpos($endpoint, 'call') !== false) return 'call';
if (strpos($endpoint, 'register') !== false) return 'register';
if (strpos($endpoint, 'config') !== false) return 'config';
return 'default';
}
/**
* Get remaining page requests
*/
public function getRemainingPageRequests($ipAddress, $endpoint, $userId = null) {
$limit = $this->getPageLimitForEndpoint($endpoint, $userId);
$sql = "SELECT COUNT(*) as request_count
FROM {$this->pagesRatelimitTable}
WHERE ip_address = :ip
AND endpoint = :endpoint
AND request_time > DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':endpoint' => $endpoint
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return max(0, $limit - $result['request_count']);
}
}

View File

@ -0,0 +1,85 @@
<?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 = [];
/**
* 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 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)) {
// remove the exact match to extrat parameters
array_shift($matches);
return $this->invoke($callback, $matches);
}
}
// if there was no match at all, return 404
http_response_code(404);
echo '404 page not found';
}
/**
* 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 = "../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,54 @@
<?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

@ -0,0 +1,276 @@
<?php
/**
* Session Class
*
* Core session management functionality for the application
*/
class Session {
private static $initialized = false;
private static $sessionName = ''; // Will be set from config, if not we'll have a random session name
/**
* Generate a random session name
*/
private static function generateRandomSessionName(): string {
return 'sess_' . bin2hex(random_bytes(8)); // 16-character random string
}
private static $sessionOptions = [
'cookie_httponly' => 1,
'cookie_secure' => 0,
'cookie_samesite' => 'Lax',
'gc_maxlifetime' => 7200 // 2 hours
];
/**
* Initialize session configuration
*/
private static function initialize() {
if (self::$initialized) {
return;
}
global $config;
// Get session name from config or generate a random one
self::$sessionName = $config['session']['name'] ?? self::generateRandomSessionName();
// Set session name before starting the session, only if headers not sent and no active session
if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
session_name(self::$sessionName);
}
// Set session cookie parameters only if headers not sent and no active session
$thisPath = $config['folder'] ?? '/';
$thisDomain = $config['domain'] ?? '';
$isSecure = isset($_SERVER['HTTPS']);
if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
session_set_cookie_params([
'lifetime' => 0, // Session cookie (browser session)
'path' => $thisPath,
'domain' => $thisDomain,
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax'
]);
}
// Align session start options dynamically with current transport
self::$sessionOptions['cookie_secure'] = $isSecure ? 1 : 0;
self::$sessionOptions['cookie_samesite'] = 'Lax';
self::$initialized = true;
}
/**
* Get session name from config or generate a random one
*/
private static function getSessionNameFromConfig($config) {
if (isset($config['session']['name']) && !empty($config['session']['name'])) {
return $config['session']['name'];
}
return self::generateRandomSessionName();
}
/**
* Start or resume a session with secure options
*/
public static function startSession() {
self::initialize();
if (session_status() === PHP_SESSION_NONE) {
if (!headers_sent()) {
session_start(self::$sessionOptions);
}
}
}
/**
* Destroy current session and clean up
*/
public static function destroySession() {
if (session_status() === PHP_SESSION_ACTIVE) {
session_unset();
session_destroy();
}
}
/**
* Get current username if set
*/
public static function getUsername() {
return isset($_SESSION['username']) ? htmlspecialchars($_SESSION['username']) : null;
}
/**
* Get current user ID if set
*/
public static function getUserId() {
return isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
}
/**
* Check if current session is valid
*
* @param bool $strict If true, will return false for new/unauthenticated sessions
* @return bool True if session is valid, false otherwise
*/
public static function isValidSession($strict = true) {
// Ensure a session is started (safe in CLI/tests)
self::startSession();
// If there is no session data at all, it's not valid
if (empty($_SESSION)) {
return false;
}
// In non-strict mode, consider empty session as valid (for login/logout)
if (!$strict && !isset($_SESSION['user_id']) && !isset($_SESSION['username'])) {
return true;
}
// In strict mode, require user_id and username
if ($strict && (!isset($_SESSION['user_id']) || !isset($_SESSION['username']))) {
return false;
}
// Check session timeout
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 7200; // 30 days or 2 hours
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) {
return false;
}
// Update last activity time
$_SESSION['LAST_ACTIVITY'] = time();
// Regenerate session ID periodically (every 30 minutes)
if (!isset($_SESSION['CREATED'])) {
$_SESSION['CREATED'] = time();
} else if (time() - $_SESSION['CREATED'] > 1800) {
// Regenerate session ID and update creation time
if (!headers_sent() && session_status() === PHP_SESSION_ACTIVE) {
$oldData = $_SESSION;
session_regenerate_id(true);
$_SESSION = $oldData;
$_SESSION['CREATED'] = time();
}
}
return true;
}
/**
* Set remember me option for extended session
*/
public static function setRememberMe($value = true) {
$_SESSION['REMEMBER_ME'] = $value;
}
/**
* Clear session data and cookies
*/
public static function cleanup($config) {
self::destroySession();
// Clear cookies if headers not sent
if (!headers_sent()) {
setcookie('username', '', [
'expires' => time() - 3600,
'path' => $config['folder'],
'domain' => $config['domain'],
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax'
]);
}
// Start fresh session
self::startSession();
// Reset session timeout flag
unset($_SESSION['session_timeout_shown']);
}
/**
* Create a new authenticated session for a user
*/
public static function createAuthSession($userId, $username, $rememberMe, $config) {
// Ensure session is started
self::startSession();
// Set session variables
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $username;
$_SESSION['LAST_ACTIVITY'] = time();
$_SESSION['REMEMBER_ME'] = $rememberMe;
// Set cookie lifetime based on remember me
$cookieLifetime = $rememberMe ? time() + (30 * 24 * 60 * 60) : 0;
// Update session cookie with remember me setting
if (!headers_sent()) {
setcookie(
session_name(),
session_id(),
[
'expires' => $cookieLifetime,
'path' => $config['folder'] ?? '/',
'domain' => $config['domain'] ?? '',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax'
]
);
// Set username cookie
setcookie('username', $username, [
'expires' => $cookieLifetime,
'path' => $config['folder'] ?? '/',
'domain' => $config['domain'] ?? '',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax'
]);
}
if ($rememberMe) {
self::setRememberMe(true);
}
}
/**
* Store 2FA pending information in session
*/
public static function store2FAPending($userId, $username, $rememberMe = false) {
$_SESSION['2fa_pending_user_id'] = $userId;
$_SESSION['2fa_pending_username'] = $username;
if ($rememberMe) {
$_SESSION['2fa_pending_remember'] = true;
}
}
/**
* Clear 2FA pending information from session
*/
public static function clear2FAPending() {
unset($_SESSION['2fa_pending_user_id']);
unset($_SESSION['2fa_pending_username']);
unset($_SESSION['2fa_pending_remember']);
}
/**
* Get 2FA pending information
*/
public static function get2FAPending() {
if (!isset($_SESSION['2fa_pending_user_id']) || !isset($_SESSION['2fa_pending_username'])) {
return null;
}
return [
'user_id' => $_SESSION['2fa_pending_user_id'],
'username' => $_SESSION['2fa_pending_username'],
'remember_me' => isset($_SESSION['2fa_pending_remember'])
];
}
}

View File

@ -0,0 +1,97 @@
<?php
/**
* class Settings
*
* Handles editing and fetching jilo configuration.
*/
class Settings {
/**
* Loads javascript file the Jitsi server.
*
* @param string $jitsiUrl The base URL of the Jitsi server.
* @param string $livejsFile The name of the remote js file to load.
* @param bool $raw Whether to return the full file (true) or only uncommented values (false).
*
* @return string The content of the interface_config.js file or an error message.
*/
public function getPlatformJsFile($jitsiUrl, $livejsFile, $raw = false) {
// constructing the URL
$jsFile = $jitsiUrl . '/' . $livejsFile;
// default content, if we can't get the file contents
$jsFileContent = "The file $livejsFile can't be loaded.";
// Check if URL is valid
if (!filter_var($jsFile, FILTER_VALIDATE_URL)) {
return "Invalid URL: $jsFile";
}
// ssl options
$contextOptions = [
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
];
$context = stream_context_create($contextOptions);
// Try to get headers first to check if file exists and wasn't redirected
$headers = @get_headers($jsFile, 1); // 1 to get headers as array
if ($headers === false) {
return "The file $livejsFile can't be loaded (connection error).";
}
// Check for redirects
$statusLine = $headers[0];
if (strpos($statusLine, '301') !== false || strpos($statusLine, '302') !== false) {
return "The file $livejsFile was redirected - this might indicate the file doesn't exist.";
}
// Check if we got 200 OK
if (strpos($statusLine, '200') === false) {
return "The file $livejsFile can't be loaded (HTTP error: $statusLine).";
}
// Check content type
$contentType = isset($headers['Content-Type']) ? $headers['Content-Type'] : '';
if (is_array($contentType)) {
$contentType = end($contentType); // get last content-type in case of redirects
}
if (stripos($contentType, 'javascript') === false && stripos($contentType, 'text/plain') === false) {
return "The file $livejsFile doesn't appear to be a JavaScript file (got $contentType).";
}
// get the file
$fileContent = @file_get_contents($jsFile, false, $context);
if ($fileContent !== false) {
// Quick validation of content
$firstLine = strtolower(trim(substr($fileContent, 0, 100)));
if (strpos($firstLine, '<!doctype html>') !== false ||
strpos($firstLine, '<html') !== false ||
strpos($firstLine, '<?xml') !== false) {
return "The file $livejsFile appears to be HTML/XML content instead of JavaScript.";
}
// when we need only uncommented values
if ($raw === false) {
// remove block comments
$jsFileContent = preg_replace('!/\*.*?\*/!s', '', $fileContent);
// remove single-line comments
$jsFileContent = preg_replace('/\/\/[^\n]*/', '', $jsFileContent);
// remove empty lines
$jsFileContent = preg_replace('/^\s*[\r\n]/m', '', $jsFileContent);
// when we need the full file as it is
} else {
$jsFileContent = $fileContent;
}
}
return $jsFileContent;
}
}

View File

@ -0,0 +1,451 @@
<?php
use App\App;
// Already required in index.php, but we require it here,
// because this class could be used standalone
require_once __DIR__ . '/../helpers/logger_loader.php';
/**
* Class TwoFactorAuthentication
*
* Handles two-factor authentication functionality using TOTP (Time-based One-Time Password).
* Internal implementation without external dependencies.
*/
class TwoFactorAuthentication {
private $db;
private $secretLength = 20; // 160 bits for SHA1
private $period = 30; // Time step in seconds (T0)
private $digits = 6; // Number of digits in TOTP code
private $algorithm = 'sha1'; // HMAC algorithm
// Branding: populated from config so authenticator apps show the configured site name.
private $issuer = 'Website';
private $window = 1; // Time window of 1 step before/after
/**
* Constructor
*
* @param PDO $database Database connection
*/
public function __construct($database) {
if ($database instanceof PDO) {
$this->db = $database;
} else {
$this->db = $database->getConnection();
}
$config = App::config();
$this->issuer = (string)$config['site_name'];
}
/**
* Enable 2FA for a user
*
* @param int $userId User ID
* @param string $secret Secret key (base32 encoded)
* @param string $code Verification code
* @return bool True if enabled successfully
*/
public function enable($userId, $secret = null, $code = null) {
try {
// Check if 2FA is already enabled
$stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?');
$stmt->execute([$userId]);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existing && $existing['enabled']) {
return false;
}
// If no secret provided, generate one and return setup data
if ($secret === null) {
// Generate secret key
$secret = $this->generateSecret();
// Get user's username for the QR code
$stmt = $this->db->prepare('SELECT username FROM user WHERE id = ?');
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// Generate backup codes
$backupCodes = $this->generateBackupCodes();
// Store in database without enabling yet
$this->db->beginTransaction();
$stmt = $this->db->prepare('
INSERT INTO user_2fa (user_id, secret_key, backup_codes, enabled, created_at)
VALUES (?, ?, ?, 0, NOW())
ON DUPLICATE KEY UPDATE
secret_key = VALUES(secret_key),
backup_codes = VALUES(backup_codes),
enabled = VALUES(enabled),
created_at = VALUES(created_at)
');
$stmt->execute([
$userId,
$secret,
json_encode($backupCodes)
]);
$this->db->commit();
// Generate otpauth URL for QR code
$otpauthUrl = $this->generateOtpauthUrl($user['username'], $secret);
return [
'success' => true,
'data' => [
'secret' => $secret,
'otpauthUrl' => $otpauthUrl,
'backupCodes' => $backupCodes
]
];
}
// If secret and code provided, verify the code and enable 2FA
if ($code !== null) {
// Verify the setup code
if (!$this->verify($userId, $code)) {
app_log('warning', '2FA setup code verification failed', [
'scope' => 'security',
'user_id' => $userId,
]);
return false;
}
// Enable 2FA
$stmt = $this->db->prepare('
UPDATE user_2fa
SET enabled = 1
WHERE user_id = ? AND secret_key = ?
');
return $stmt->execute([$userId, $secret]);
}
return false;
} catch (Exception $e) {
if ($this->db->inTransaction()) {
$this->db->rollBack();
}
app_log('error', '2FA enable error: ' . $e->getMessage(), [
'scope' => 'security',
'user_id' => $userId,
]);
return false;
}
}
/**
* Verify a 2FA code
*
* @param int $userId User ID
* @param string $code The verification code
* @return bool True if verified, false otherwise
*/
public function verify($userId, $code) {
try {
// Get user's 2FA settings
$settings = $this->getUserSettings($userId);
if (!$settings) {
return false;
}
// Check if code matches a backup code
if ($this->verifyBackupCode($userId, $code)) {
return true;
}
// Get current Unix timestamp
$currentTime = time();
// Check time window
for ($timeSlot = -$this->window; $timeSlot <= $this->window; $timeSlot++) {
$checkTime = $currentTime + ($timeSlot * $this->period);
$generatedCode = $this->generateCode($settings['secret_key'], $checkTime);
if (hash_equals($generatedCode, $code)) {
return true;
}
}
return false;
} catch (Exception $e) {
app_log('error', '2FA verification error: ' . $e->getMessage(), [
'scope' => 'security',
'user_id' => $userId,
]);
return false;
}
}
/**
* Generate a random secret key
*
* @return string Base32 encoded secret
*/
private function generateSecret() {
// Generate random bytes (160 bits for SHA1)
$random = random_bytes($this->secretLength);
return $this->base32Encode($random);
}
/**
* Base32 encode data
*
* @param string $data Data to encode
* @return string Base32 encoded string
*/
private function base32Encode($data) {
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$binary = '';
$encoded = '';
// Convert to binary
for ($i = 0; $i < strlen($data); $i++) {
$binary .= str_pad(decbin(ord($data[$i])), 8, '0', STR_PAD_LEFT);
}
// Process 5 bits at a time
for ($i = 0; $i < strlen($binary); $i += 5) {
$chunk = substr($binary, $i, 5);
if (strlen($chunk) < 5) {
$chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
}
$encoded .= $alphabet[bindec($chunk)];
}
// Add padding
$padding = strlen($encoded) % 8;
if ($padding > 0) {
$encoded .= str_repeat('=', 8 - $padding);
}
return $encoded;
}
/**
* Base32 decode data
*
* @param string $data Base32 encoded string
* @return string Decoded data
*/
private function base32Decode($data) {
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
// Remove padding and uppercase
$data = rtrim(strtoupper($data), '=');
$binary = '';
// Convert to binary
for ($i = 0; $i < strlen($data); $i++) {
$position = strpos($alphabet, $data[$i]);
if ($position === false) {
continue;
}
$binary .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT);
}
$decoded = '';
// Process 8 bits at a time
for ($i = 0; $i + 7 < strlen($binary); $i += 8) {
$chunk = substr($binary, $i, 8);
$decoded .= chr(bindec($chunk));
}
return $decoded;
}
/**
* Generate a TOTP code for a given secret and time
* RFC 6238 compliant implementation
*/
private function generateCode($secret, $time) {
// Calculate number of time steps since Unix epoch
$timeStep = (int)floor($time / $this->period);
// Pack time into 8 bytes (64-bit big-endian)
$timeBin = pack('J', $timeStep);
// Clean secret of any padding
$secret = rtrim($secret, '=');
// Get binary secret
$secretBin = $this->base32Decode($secret);
// Calculate HMAC
$hash = hash_hmac($this->algorithm, $timeBin, $secretBin, true);
// Get dynamic truncation offset
$offset = ord($hash[strlen($hash) - 1]) & 0xF;
// Generate 31-bit number
$code = (
((ord($hash[$offset]) & 0x7F) << 24) |
((ord($hash[$offset + 1]) & 0xFF) << 16) |
((ord($hash[$offset + 2]) & 0xFF) << 8) |
(ord($hash[$offset + 3]) & 0xFF)
) % pow(10, $this->digits);
$code = str_pad($code, $this->digits, '0', STR_PAD_LEFT);
return $code;
}
/**
* Generate otpauth URL for QR codes
* Format: otpauth://totp/ISSUER:ACCOUNT?secret=SECRET&issuer=ISSUER&algorithm=ALGORITHM&digits=DIGITS&period=PERIOD
*/
private function generateOtpauthUrl($username, $secret) {
$params = [
'secret' => $secret,
'issuer' => $this->issuer,
'algorithm' => strtoupper($this->algorithm),
'digits' => $this->digits,
'period' => $this->period
];
return sprintf(
'otpauth://totp/%s:%s?%s',
rawurlencode($this->issuer),
rawurlencode($username),
http_build_query($params)
);
}
/**
* Generate backup codes
*
* @param int $count Number of backup codes to generate
* @return array Array of backup codes
*/
private function generateBackupCodes($count = 8) {
$codes = [];
for ($i = 0; $i < $count; $i++) {
$codes[] = bin2hex(random_bytes(4));
}
return $codes;
}
/**
* Verify a backup code
*
* @param int $userId User ID
* @param string $code The backup code to verify
* @return bool True if verified, false otherwise
*/
private function verifyBackupCode($userId, $code) {
try {
$stmt = $this->db->prepare('SELECT backup_codes FROM user_2fa WHERE user_id = ?');
$stmt->execute([$userId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result) {
return false;
}
$backupCodes = json_decode($result['backup_codes'], true);
// Check if the code exists and hasn't been used
$codeIndex = array_search($code, $backupCodes);
if ($codeIndex !== false) {
// Remove the used code
unset($backupCodes[$codeIndex]);
$backupCodes = array_values($backupCodes);
// Update backup codes in database
$stmt = $this->db->prepare('
UPDATE user_2fa
SET backup_codes = ?
WHERE user_id = ?
');
$stmt->execute([json_encode($backupCodes), $userId]);
return true;
}
return false;
} catch (Exception $e) {
app_log('error', 'Backup code verification error: ' . $e->getMessage(), [
'scope' => 'security',
'user_id' => $userId,
]);
return false;
}
}
/**
* Disable 2FA for a user
*
* @param int $userId User ID
* @return bool True if disabled successfully
*/
public function disable($userId) {
try {
// First check if user has 2FA settings
$settings = $this->getUserSettings($userId);
if (!$settings) {
return false;
}
// Delete the 2FA settings entirely instead of just disabling
$stmt = $this->db->prepare('
DELETE FROM user_2fa
WHERE user_id = ?
');
return $stmt->execute([$userId]);
} catch (Exception $e) {
app_log('error', '2FA disable error: ' . $e->getMessage(), [
'scope' => 'security',
'user_id' => $userId,
]);
return false;
}
}
/**
* Check if 2FA is enabled for a user
*
* @param int $userId User ID
* @return bool True if enabled
*/
public function isEnabled($userId) {
try {
$stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?');
$stmt->execute([$userId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result && $result['enabled'];
} catch (Exception $e) {
app_log('error', '2FA status check error: ' . $e->getMessage(), [
'scope' => 'security',
'user_id' => $userId,
]);
return false;
}
}
private function getUserSettings($userId) {
try {
$stmt = $this->db->prepare('
SELECT secret_key, backup_codes, enabled
FROM user_2fa
WHERE user_id = ?
');
$stmt->execute([$userId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
} catch (Exception $e) {
app_log('error', 'Failed to get user 2FA settings: ' . $e->getMessage(), [
'scope' => 'security',
'user_id' => $userId,
]);
return null;
}
}
}

View File

@ -0,0 +1,646 @@
<?php
use App\App;
/**
* class User
*
* Handles user-related functionalities such as login, rights management, and profile updates.
*/
class User {
/**
* @var PDO|null $db The database connection instance.
*/
private $db;
private $rateLimiter;
private $twoFactorAuth;
/**
* Cache for database schema checks
* @var array<string,bool>
*/
private static $schemaCache = [];
/**
* User constructor.
* Initializes the database connection.
*
* @param object $database The database object to initialize the connection.
*/
public function __construct($database) {
if ($database instanceof PDO) {
$this->db = $database;
} else {
$this->db = $database->getConnection();
}
require_once __DIR__ . '/ratelimiter.php';
require_once __DIR__ . '/twoFactorAuth.php';
$this->rateLimiter = new RateLimiter();
$this->twoFactorAuth = new TwoFactorAuthentication($database);
}
/**
* Check if a column exists in a given table. Results are cached per request.
*
* @param string $table
* @param string $column
* @return bool
*/
private function columnExists(string $table, string $column): bool {
$cacheKey = $table . '.' . $column;
if (isset(self::$schemaCache[$cacheKey])) {
return self::$schemaCache[$cacheKey];
}
try {
$stmt = $this->db->prepare("SHOW COLUMNS FROM `$table` LIKE :column");
$stmt->execute([':column' => $column]);
$exists = (bool)$stmt->fetch(PDO::FETCH_ASSOC);
self::$schemaCache[$cacheKey] = $exists;
return $exists;
} catch (Exception $e) {
// On error, assume column doesn't exist to be safe
self::$schemaCache[$cacheKey] = false;
return false;
}
}
/**
* Get the user's preferred theme if stored in DB (user_meta.theme). Returns null if not set.
*
* @param int $userId
* @return string|null
*/
public function getUserTheme(int $userId): ?string {
if (!$this->columnExists('user_meta', 'theme')) {
return null;
}
try {
$sql = 'SELECT theme FROM user_meta WHERE user_id = :user_id LIMIT 1';
$stmt = $this->db->prepare($sql);
$stmt->execute([':user_id' => $userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
$theme = $row['theme'] ?? null;
return ($theme !== null && $theme !== '') ? $theme : null;
} catch (Exception $e) {
return null;
}
}
/**
* Persist the user's preferred theme in DB (user_meta.theme) when the column exists.
* Silently no-ops if the column is missing.
*
* @param int $userId
* @param string $theme
* @return bool True when stored or safely skipped; false only on explicit DB error.
*/
public function setUserTheme(int $userId, string $theme): bool {
if (!$this->columnExists('user_meta', 'theme')) {
// Column not present; treat as success to avoid breaking UX
return true;
}
try {
$sql = 'UPDATE user_meta SET theme = :theme WHERE user_id = :user_id';
$stmt = $this->db->prepare($sql);
$ok = $stmt->execute([':theme' => $theme, ':user_id' => $userId]);
return (bool)$ok;
} catch (Exception $e) {
return false;
}
}
/**
* Logs in a user by verifying credentials.
*
* @param string $username The username of the user.
* @param string $password The password of the user.
* @param string $twoFactorCode Optional. The 2FA code if 2FA is enabled.
*
* @return array Login result with status and any necessary data
*/
public function login($username, $password, $twoFactorCode = null) {
// Get user's IP address
$ipAddress = getUserIP();
// 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 user WHERE username = :username");
$query->bindParam(':username', $username);
$query->execute();
$user = $query->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password'])) {
// Check if 2FA is enabled
if ($this->twoFactorAuth->isEnabled($user['id'])) {
if ($twoFactorCode === null) {
return [
'status' => 'requires_2fa',
'user_id' => $user['id'],
'username' => $user['username']
];
}
// Verify 2FA code
if (!$this->twoFactorAuth->verify($user['id'], $twoFactorCode)) {
return [
'status' => 'invalid_2fa',
'message' => 'Invalid 2FA code'
];
}
}
// Login successful
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['CREATED'] = time();
$_SESSION['LAST_ACTIVITY'] = time();
return [
'status' => 'success',
'user_id' => $user['id'],
'username' => $user['username']
];
}
// Get remaining attempts AFTER this failed attempt
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
return [
'status' => 'failed',
'message' => "Invalid credentials. {$remainingAttempts} attempts remaining."
];
}
/**
* 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 user WHERE username = :username';
$query = $this->db->prepare($sql);
$query->bindParam(':username', $username);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Fetches user details by user ID.
*
* @param int $userId The user ID.
*
* @return array|null User details or null if not found.
*/
public function getUserDetails($userId) {
$sql = 'SELECT
um.*,
u.username
FROM
user_meta um
LEFT JOIN user u
ON um.user_id = u.id
WHERE
u.id = :user_id';
$query = $this->db->prepare($sql);
$query->execute([
':user_id' => $userId,
]);
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Grants a user a specific right.
*
* @param int $userId The user ID.
* @param int $right_id The right ID to grant.
*
* @return void
*/
public function addUserRight($userId, $right_id) {
$sql = 'INSERT INTO user_right
(user_id, right_id)
VALUES
(:user_id, :right_id)';
$query = $this->db->prepare($sql);
$query->execute([
':user_id' => $userId,
':right_id' => $right_id,
]);
}
/**
* Revokes a specific right from a user.
*
* @param int $userId The user ID.
* @param int $right_id The right ID to revoke.
*
* @return void
*/
public function removeUserRight($userId, $right_id) {
$sql = 'DELETE FROM user_right
WHERE
user_id = :user_id
AND
right_id = :right_id';
$query = $this->db->prepare($sql);
$query->execute([
':user_id' => $userId,
':right_id' => $right_id,
]);
}
/**
* Retrieves all rights in the system.
*
* @return array List of rights.
*/
public function getAllRights() {
$sql = 'SELECT
id AS right_id,
name AS right_name
FROM `right`
ORDER BY id ASC';
$query = $this->db->prepare($sql);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Retrieves the rights assigned to a specific user.
*
* @param int $userId The user ID.
*
* @return array List of user rights.
*/
public function getUserRights($userId) {
$sql = 'SELECT
u.id AS user_id,
r.id AS right_id,
r.name AS right_name
FROM
`user` u
LEFT JOIN `user_right` ur
ON u.id = ur.user_id
LEFT JOIN `right` r
ON ur.right_id = r.id
WHERE
u.id = :user_id';
$query = $this->db->prepare($sql);
$query->execute([
':user_id' => $userId,
]);
$result = $query->fetchAll(PDO::FETCH_ASSOC);
// ensure specific entries are included in the result
$specialEntries = [];
// user 1 is always superuser
if ($userId == 1) {
$specialEntries = [
[
'user_id' => 1,
'right_id' => 1,
'right_name' => 'superuser'
]
];
// user 2 is always demo
} elseif ($userId == 2) {
$specialEntries = [
[
'user_id' => 2,
'right_id' => 100,
'right_name' => 'demo user'
]
];
}
// merge the special entries with the existing results
$result = array_merge($specialEntries, $result);
// remove duplicates if necessary
$result = array_unique($result, SORT_REGULAR);
// return the modified result
return $result;
}
/**
* Check if the user has a specific right.
*
* @param int $userId The user ID.
* @param string $right_name The human-readable name of the user right.
*
* @return bool True if the user has the right, false otherwise.
*/
function hasRight($userId, $right_name) {
$userRights = $this->getUserRights($userId);
$userHasRight = false;
// superuser always has all the rights
if ($userId === 1) {
$userHasRight = true;
}
foreach ($userRights as $right) {
if ($right['right_name'] === $right_name) {
$userHasRight = true;
break;
}
}
return $userHasRight;
}
/**
* Updates a user's metadata in the database.
*
* @param int $userId 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($userId, $updatedUser) {
try {
$sql = 'UPDATE user_meta SET
name = :name,
email = :email,
timezone = :timezone,
bio = :bio
WHERE user_id = :user_id';
$query = $this->db->prepare($sql);
$query->execute([
':user_id' => $userId,
':name' => $updatedUser['name'],
':email' => $updatedUser['email'],
':timezone' => $updatedUser['timezone'],
':bio' => $updatedUser['bio']
]);
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* Removes a user's avatar from the database and deletes the associated file.
*
* @param int $userId The ID of the user whose avatar is being removed.
* @param string $old_avatar Optional. The file path of the current avatar to delete. Default is an empty string.
*
* @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs.
*/
public function removeAvatar($userId, $old_avatar = '') {
try {
// remove from database
$sql = 'UPDATE user_meta SET
avatar = NULL
WHERE user_id = :user_id';
$query = $this->db->prepare($sql);
$query->execute([
':user_id' => $userId,
]);
// delete the old avatar file
if ($old_avatar && file_exists($old_avatar)) {
unlink($old_avatar);
}
return true;
} catch (Exception $e) {
return $e->getMessage();
}
}
/**
* Updates a user's avatar by uploading a new file and saving its path in the database.
*
* @param int $userId The ID of the user whose avatar is being updated.
* @param array $avatar_file The uploaded avatar file from the $_FILES array.
* Should include 'tmp_name', 'name', 'error', etc.
* @param string $avatars_path The directory path where avatar files should be saved.
*
* @return bool|string Returns true if the avatar is successfully updated, or an error message if an exception occurs.
*/
public function changeAvatar($userId, $avatar_file, $avatars_path) {
try {
// check if the file was uploaded
if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) {
$fileTmpPath = $avatar_file['tmp_name'];
$fileName = $avatar_file['name'];
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
// validate file extension
if (in_array($fileExtension, ['jpg', 'png', 'jpeg'])) {
$newFileName = md5(time() . $fileName) . '.' . $fileExtension;
$dest_path = $avatars_path . $newFileName;
// ensure avatars directory exists
if (!is_dir($avatars_path)) {
if (!mkdir($avatars_path, 0755, true)) {
$_SESSION['error'] .= 'Unable to create avatars directory. ';
return false;
}
}
// check if directory is writable
if (!is_writable($avatars_path)) {
$_SESSION['error'] .= 'Avatars directory is not writable. ';
return false;
}
// move the file to avatars folder
if (move_uploaded_file($fileTmpPath, $dest_path)) {
try {
// update user's avatar path in DB
$sql = 'UPDATE user_meta SET
avatar = :avatar
WHERE user_id = :user_id';
$query = $this->db->prepare($sql);
$query->execute([
':avatar' => $newFileName,
':user_id' => $userId
]);
// all went OK
$_SESSION['notice'] = 'Avatar updated successfully. ';
return true;
} catch (Exception $e) {
$_SESSION['error'] .= 'Database error updating avatar. ';
return $e->getMessage();
}
} else {
$_SESSION['error'] = 'Error moving the uploaded file. Please check directory permissions. ';
}
} else {
$_SESSION['error'] = 'Invalid avatar file type. Only JPG, PNG, and JPEG are allowed. ';
}
} else {
// Handle different upload errors
switch ($avatar_file['error']) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$_SESSION['error'] = 'Avatar file is too large. Maximum size is 500KB. ';
break;
case UPLOAD_ERR_PARTIAL:
$_SESSION['error'] = 'Avatar file was only partially uploaded. ';
break;
case UPLOAD_ERR_NO_FILE:
$_SESSION['error'] = 'No avatar file was uploaded. ';
break;
case UPLOAD_ERR_NO_TMP_DIR:
$_SESSION['error'] = 'Missing temporary folder for file upload. ';
break;
case UPLOAD_ERR_CANT_WRITE:
$_SESSION['error'] = 'Failed to write avatar file to disk. ';
break;
case UPLOAD_ERR_EXTENSION:
$_SESSION['error'] = 'File upload stopped by extension. ';
break;
default:
$_SESSION['error'] = 'Unknown upload error occurred. ';
break;
}
}
} catch (Exception $e) {
$_SESSION['error'] = 'An error occurred while processing the avatar: ' . $e->getMessage();
return $e->getMessage();
}
return false;
}
/**
* Get all users for messaging
*
* @return array List of users with their IDs and usernames
*/
public function getUsers() {
$sql = "SELECT id, username
FROM `user`
ORDER BY username ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Enable two-factor authentication for a user
*
* @param int $userId User ID
* @param string $secret Secret key to use
* @param string $code Verification code to validate
* @return bool True if enabled successfully
*/
public function enableTwoFactor($userId, $secret = null, $code = null) {
return $this->twoFactorAuth->enable($userId, $secret, $code);
}
/**
* Disable two-factor authentication for a user
*
* @param int $userId User ID
* @return bool True if disabled successfully
*/
public function disableTwoFactor($userId) {
return $this->twoFactorAuth->disable($userId);
}
/**
* Verify a two-factor authentication code
*
* @param int $userId User ID
* @param string $code The verification code
* @return bool True if verified
*/
public function verifyTwoFactor($userId, $code) {
return $this->twoFactorAuth->verify($userId, $code);
}
/**
* Check if two-factor authentication is enabled for a user
*
* @param int $userId User ID
* @return bool True if enabled
*/
public function isTwoFactorEnabled($userId) {
return $this->twoFactorAuth->isEnabled($userId);
}
/**
* Change a user's password
*
* @param int $userId User ID
* @param string $currentPassword Current password for verification
* @param string $newPassword New password to set
* @return bool True if password was changed successfully
*/
public function changePassword($userId, $currentPassword, $newPassword) {
try {
// First verify the current password
$sql = "SELECT password FROM user WHERE id = :user_id";
$query = $this->db->prepare($sql);
$query->execute([':user_id' => $userId]);
$user = $query->fetch(PDO::FETCH_ASSOC);
if (!$user || !password_verify($currentPassword, $user['password'])) {
return false;
}
// Hash the new password
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
// Update the password
$sql = "UPDATE user SET password = :password WHERE id = :user_id";
$query = $this->db->prepare($sql);
return $query->execute([
':password' => $hashedPassword,
':user_id' => $userId
]);
} catch (Exception $e) {
error_log("Error changing password: " . $e->getMessage());
return false;
}
}
}

View File

@ -0,0 +1,130 @@
<?php
class Validator {
private $errors = [];
private $data = [];
public function __construct(array $data) {
$this->data = $data;
}
public function validate(array $rules) {
foreach ($rules as $field => $fieldRules) {
foreach ($fieldRules as $rule => $parameter) {
$this->applyRule($field, $rule, $parameter);
}
}
return empty($this->errors);
}
private function applyRule($field, $rule, $parameter) {
$value = $this->data[$field] ?? null;
switch ($rule) {
// case for required fields that can be empty strings
case 'required':
if ($parameter && empty($value)) {
$label = $this->formatFieldLabel($field);
$this->addError($field, "$label is required");
}
break;
// special case for required fields that can't be empty strings or null
case 'required_strict':
if ($parameter) {
if ($value === null) {
$label = $this->formatFieldLabel($field);
$this->addError($field, "$label is required");
} elseif (is_string($value)) {
if (trim($value) === '') {
$label = $this->formatFieldLabel($field);
$this->addError($field, "$label is required");
}
}
}
break;
case 'email':
if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$this->addError($field, "Invalid email format");
}
break;
case 'min':
if (!empty($value) && strlen($value) < $parameter) {
$this->addError($field, "Minimum length is $parameter characters");
}